Merge branch '17.0' of github.com:Dur-Pro/bemade-addons into 17.0

This commit is contained in:
Benoît Vézina 2024-03-21 13:16:28 -04:00
commit 61b555f78a
126 changed files with 2723 additions and 1096 deletions

33
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: tests
on:
pull_request:
branches:
- "17.0-test-ci"
push:
branches:
- "17.0-test-ci"
jobs:
test:
runs-on: ubuntu-22.04
container: ghcr.io/bemade/test-odoo_arm64:latest
name: Test Repo Addons With Odoo
strategy:
fail-fast: false
services:
postgres:
image: postgres:12.0
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
POSTGRES_DB: odoo
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
with:
persist-credientials: false
- name: Run Tests
run: run_tests.sh

View file

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

View file

@ -0,0 +1,21 @@
{
'name': 'Account Credit Hold',
'version': '17.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,7 +1,7 @@
# -*- coding: utf-8 -*-
{
"name": "Change default when adding follower",
"version": "15.0.0.0.1",
"version": "17.0.0.0.1",
"category": "Extra Tools",
'summary': 'Change default when adding follower',
"description": """

View file

@ -5,4 +5,7 @@ from odoo import models, fields, api
class MailWizardInviteDefault(models.TransientModel):
_inherit = 'mail.wizard.invite'
send_mail = fields.Boolean(default=False, help="If true, an invitation email will be sent to the recipient")
send_mail = fields.Boolean(
default=False,
help="If true, an invitation email will be sent to the recipient"
)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
"name": "Fetchmail Only on production environment",
"version": "15.0.0.0.1",
"version": "17.0.0.0.1",
"category": "Extra Tools",
'summary': 'Fetchmail Only on production environment',
"description": """
@ -10,7 +10,7 @@
"author": "Bemade",
'website': 'https://www.bemade.org',
"depends": [
'fetchmail',
'mail',
],
"data": [
],

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,17 +52,21 @@
'views/menus.xml',
'views/task_views.xml',
'views/sale_order_views.xml',
# 'reports/worksheet_custom_report_templates.xml',
# 'reports/worksheet_custom_reports.xml',
'reports/worksheet_custom_report_templates.xml',
'reports/worksheet_custom_reports.xml',
'wizard/new_task_from_template.xml',
],
'assets': {
'web.assets_tests': [
'bemade_fsm/static/tests/tours/task_template_tour.js',
'bemade_fsm/static/tests/tours/equipment_tour.js',
'bemade_fsm/static/tests/tours/sale_order_tour.js',
],
'web.report_assets_common': [
'bemade_fsm/static/src/scss/bemade_fsm.scss'
],
'web.assets_backend': [
# BV: need to readd these files
# '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

@ -4,7 +4,8 @@
<record id="planning_project_stage_waiting_parts" model="project.task.type">
<field name="sequence">2</field>
<field name="name">Waiting on Parts</field>
<field name="legend_blocked">Blocked</field>
<!-- BV: legend_blocked n'existe plus -->
<!-- BV: <field name="legend_blocked">Blocked</field>-->
<field name="fold" eval="False"/>
<!-- <field name="is_closed" eval="False"/>-->
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
@ -12,7 +13,7 @@
<record id="planning_project_stage_work_completed" model="project.task.type">
<field name="sequence">15</field>
<field name="name">Work Executed</field>
<field name="legend_blocked">Blocked</field>
<!-- BV: <field name="legend_blocked">Blocked</field>-->
<field name="fold" eval="False"/>
<!-- <field name="is_closed" eval="False"/>-->
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
@ -20,7 +21,7 @@
<record id="planning_project_stage_exception" model="project.task.type">
<field name="sequence">19</field>
<field name="name">Exception</field>
<field name="legend_blocked">Blocked</field>
<!-- BV: <field name="legend_blocked">Blocked</field>-->
<field name="fold" eval="False"/>
<!-- <field name="is_closed" eval="False"/>-->
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]"/>
@ -35,8 +36,9 @@
<record id="industry_fsm.fsm_project" model="project.project">
<field name="type_ids"
eval="[(4, ref('industry_fsm.planning_project_stage_0')), (4, ref('industry_fsm.planning_project_stage_1')), (4, ref('industry_fsm.planning_project_stage_2')), (4, ref('planning_project_stage_work_completed')), (4, ref('industry_fsm.planning_project_stage_3')), (4, ref('industry_fsm.planning_project_stage_4'))]"/>
<field name="allow_subtasks"
eval="True"/>
<!-- BV: allow_subtask n'existe plus-->
<!-- <field name="allow_subtasks"-->
<!-- eval="True"/>-->
</record>
<function model="ir.model.data" name="write">
<function name="search" model="ir.model.data">

View file

@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-02 20:26+0000\n"
"PO-Revision-Date: 2023-10-02 20:26+0000\n"
"POT-Creation-Date: 2024-02-13 17:09+0000\n"
"PO-Revision-Date: 2024-02-13 17:09+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -16,7 +16,7 @@ msgstr ""
"Plural-Forms: \n"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid ""
"<span groups=\"account.group_show_line_subtotals_tax_excluded\">\n"
" Amount</span>\n"
@ -24,9 +24,9 @@ msgid ""
" Total Price</span>"
msgstr ""
"<span groups=\"account.group_show_line_subtotals_tax_excluded\">\n"
" Prix</span>\n"
" Montant</span>\n"
" <span groups=\"account.group_show_line_subtotals_tax_included\">\n"
" Prix</span>"
" Prix Total</span>"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_signature_block
@ -40,12 +40,12 @@ msgstr ""
" </span>"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "<span>Disc.%</span>"
msgstr "<span>Escompte %</span>"
msgstr "<span>% Escompte</span>"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid ""
"<strong class=\"mr16\">Section\n"
" Subtotal</strong>"
@ -54,19 +54,19 @@ msgstr ""
" Sous-total</strong>"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "<strong>Taxes</strong>"
msgstr ""
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "<strong>Total</strong>"
msgstr ""
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "<strong>Untaxed amount</strong>"
msgstr "<strong>Montant hors taxes</strong>"
msgstr "<strong>Montant hors-taxes</strong>"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_needaction
@ -78,20 +78,15 @@ msgstr "Action requise"
msgid "Activities"
msgstr "Activités"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_exception_decoration
msgid "Activity Exception Decoration"
msgstr ""
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_state
msgid "Activity State"
msgstr ""
msgstr "État de l'activité"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_type_icon
msgid "Activity Type Icon"
msgstr ""
msgstr "Icône du type d'activité"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__tag_ids
@ -119,14 +114,12 @@ msgstr "Bloqué"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__allow_billable
msgid "Can be billed"
msgstr ""
msgstr "Peut être facturé"
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_res_partner__is_site_contact
#: model:ir.model.fields,help:bemade_fsm.field_res_users__is_site_contact
msgid ""
"Check if the contact is a site contact, otherwise it is a not in that list"
msgstr ""
#: model_terms:ir.ui.view,arch_db:bemade_fsm.project_task_from_template_wizard_view_form
msgid "Cancel"
msgstr "Annuler"
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__tag_ids
@ -178,11 +171,37 @@ msgstr ""
msgid "Contacts and Equipment"
msgstr "Contacts et équipements"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.project_task_from_template_wizard_view_form
msgid "Create Task"
msgstr "Créer une tâche"
#. module: bemade_fsm
#. openerp-web
#: code:addons/bemade_fsm/static/src/xml/project_view_buttons.xml:0
#: code:addons/bemade_fsm/static/src/xml/project_view_buttons.xml:0
#, python-format
msgid "Create Task from Template"
msgstr "Créer une tâche à partir d'un gabarit"
#. module: bemade_fsm
#: model:ir.model,name:bemade_fsm.model_project_task_from_template_wizard
msgid "Create Task from Template Wizard"
msgstr "Assistant pour création de tâche à partir d'un gabarit"
#. module: bemade_fsm
#. openerp-web
#: code:addons/bemade_fsm/static/src/xml/project_view_buttons.xml:0
#, python-format
msgid "Create from Template"
msgstr "Créer à partir d'un gabarit"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__create_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__create_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__create_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__create_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__create_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__create_uid
msgid "Created by"
msgstr "Créé par"
@ -192,6 +211,7 @@ msgstr "Créé par"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__create_date
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__create_date
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__create_date
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__create_date
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__create_date
msgid "Created on"
msgstr "Date de création"
@ -206,6 +226,11 @@ msgstr "Client"
msgid "Customer:"
msgstr "Client :"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_timesheet_entries
msgid "Date"
msgstr ""
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__assignees
msgid "Default Assignees"
@ -247,7 +272,8 @@ msgid "Default tags for tasks created from this template."
msgstr "Étiquettes par défaut pour tâches créées à partir de ce gabarit."
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_materials_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "Delivered"
msgstr "Livré"
@ -255,7 +281,8 @@ msgstr "Livré"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__description
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__description
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_materials_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "Description"
msgstr ""
@ -274,6 +301,7 @@ msgstr "Détails"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__display_name
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__display_name
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__display_name
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__display_name
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__display_name
msgid "Display Name"
msgstr "Nom pour affichage"
@ -286,7 +314,7 @@ msgstr "Employés assignés aux tâches créées à partir de ce gabarit."
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__planned_date_end
msgid "End Date"
msgstr ""
msgstr "Date de fin"
#. module: bemade_fsm
#: model:ir.actions.act_window,name:bemade_fsm.action_window_equipment
@ -302,8 +330,9 @@ msgstr "Équipement désservi"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__equipment_count
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__equipment_count
msgid "Equipment Count"
msgstr ""
msgstr "Nombre d'équipemenets"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__complete_name
@ -313,7 +342,7 @@ msgstr "Nom d'équipement"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_equipment_summary
msgid "Equipment Serviced"
msgstr "Équipement désservi"
msgstr "Équipement(s) désservi"
#. module: bemade_fsm
#: model:ir.actions.act_window,name:bemade_fsm.action_window_equipment_tags
@ -349,11 +378,6 @@ msgstr "Durée estimée"
msgid "Exception"
msgstr ""
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__failed_message_ids
msgid "Failed Messages"
msgstr "Messages échoués"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.bemade_fsm_project_task_form_inherit
#: model_terms:ir.ui.view,arch_db:bemade_fsm.partner_equipment_location_view_form
@ -389,7 +413,7 @@ msgstr "Abonnés (partenaires)"
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_equipment__activity_type_icon
msgid "Font awesome icon e.g. fa-tasks"
msgstr ""
msgstr "Icône Font Awesome p.ex. fa-tasks"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__is_fully_delivered
@ -416,9 +440,10 @@ msgstr "A un message"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__id
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__id
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__id
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__id
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__id
msgid "ID"
msgstr ""
msgstr "Identifiant"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_exception_icon
@ -456,6 +481,8 @@ msgid ""
"Indicates whether a line or all the lines in a section have been entirely "
"delivered and invoiced."
msgstr ""
"Indique si une ligne ou toutes les lignes dans une section ont été "
"entièrement livrées et facturées."
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_visit__is_completed
@ -487,6 +514,12 @@ msgstr ""
msgid "Invoiced"
msgstr "Facturé"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__is_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__is_fsm
msgid "Is FSM"
msgstr "Est FSM"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__is_field_service
msgid "Is Field Service"
@ -501,7 +534,7 @@ msgstr "Est un abonné"
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__is_site_contact
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__is_site_contact
msgid "Is a site contact"
msgstr ""
msgstr "Est un contact sur site"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__label
@ -513,6 +546,7 @@ msgstr "Nom"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag____last_update
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type____last_update
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit____last_update
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard____last_update
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template____last_update
msgid "Last Modified on"
msgstr "Dernière modification"
@ -522,6 +556,7 @@ msgstr "Dernière modification"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__write_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__write_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__write_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__write_uid
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__write_uid
msgid "Last Updated by"
msgstr "Modifié par"
@ -531,6 +566,7 @@ msgstr "Modifié par"
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_tag__write_date
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment_type__write_date
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__write_date
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__write_date
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour"
@ -550,6 +586,11 @@ msgstr "Pièce jointe principale"
msgid "Mark as Delivered"
msgstr "Marquer comme livré"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_materials_table
msgid "Materials"
msgstr "Matériaux"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__message_has_error
msgid "Message Delivery error"
@ -560,6 +601,15 @@ msgstr "Erreur de livraison de message"
msgid "Messages"
msgstr ""
#. module: bemade_fsm
#: code:addons/bemade_fsm/models/task.py:0
#, python-format
msgid ""
"Modifying the planned start or end time on a task is not permitted when that"
" task already has planning records associated to it. Please modify or delete"
" the planning records instead."
msgstr ""
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__my_activity_date_deadline
msgid "My Activity Deadline"
@ -573,9 +623,14 @@ msgid "Name"
msgstr "Nom"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_calendar_event_id
msgid "Next Activity Calendar Event"
msgstr "Évènement au calendrier de la prochaine activité"
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__new_task_title
msgid "New Task Title"
msgstr "Nouveau titre de tâche"
#. module: bemade_fsm
#: model:ir.actions.act_window,name:bemade_fsm.action_new_task_from_template
msgid "New Task from Template"
msgstr "Nouvelle tâche à partir d'un gabarit"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_equipment__activity_date_deadline
@ -618,7 +673,8 @@ msgid "Number of unread messages"
msgstr "Nombre de messages non-lus"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_materials_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "Ordered"
msgstr "Commandé"
@ -636,6 +692,7 @@ msgid "P&ID Tag"
msgstr "Tag P&ID"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.project_task_view_search_fsm_inherit
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
msgid "Parent Task"
msgstr "Tâche parent"
@ -661,10 +718,25 @@ msgstr "Notes sur l'emplacement physique"
msgid "Plan as field service"
msgstr "Planifier comme service terrain"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
msgid "Planned end:"
msgstr "Fin planifiée :"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
msgid "Purchase Order:"
msgstr "Commande :"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
msgid "Planned start:"
msgstr "Début planifié :"
#. module: bemade_fsm
#: model:ir.model,name:bemade_fsm.model_product_template
msgid "Product Template"
msgstr "Modèle de produit"
msgstr "Modèle d'article"
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_product_product__is_field_service
@ -677,10 +749,21 @@ msgstr ""
" considéré lors de la planification."
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__project_id
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
msgid "Project"
msgstr "Projet"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__propagate_assignment
msgid "Propagate Assignment"
msgstr "Propager l'assignation"
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_project_task__propagate_assignment
msgid "Propagate assignment of this task to all subtasks."
msgstr "Propager l'assignation de cette tâche à toutes les sous-tâches."
#. module: bemade_fsm
#: model:project.task.type,legend_done:bemade_fsm.planning_project_stage_exception
#: model:project.task.type,legend_done:bemade_fsm.planning_project_stage_waiting_parts
@ -717,12 +800,17 @@ msgstr "Section de commande de vente"
#: model:ir.model,name:bemade_fsm.model_sale_order
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__sale_order_id
msgid "Sales Order"
msgstr "Commande de ventes"
msgstr "Bon de commande"
#. module: bemade_fsm
#: model:ir.model,name:bemade_fsm.model_sale_order_line
msgid "Sales Order Line"
msgstr "Ligne de commandes de vente"
msgstr "Ligne de bons de commande"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__section_line_ids
msgid "Section Line"
msgstr "Section"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_template__sequence
@ -730,7 +818,9 @@ msgid "Sequence"
msgstr "Séquence"
#. module: bemade_fsm
#: code:addons/bemade_fsm/models/sale_order.py:0
#: model:ir.model.fields,field_description:bemade_fsm.field_bemade_fsm_visit__task_id
#, python-format
msgid "Service Visit"
msgstr "Visite de service"
@ -759,13 +849,14 @@ msgstr "Contacts sur site :"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_res_partner__equipment_ids
#: model:ir.model.fields,field_description:bemade_fsm.field_res_users__equipment_ids
msgid "Site Equipment"
msgstr ""
msgstr "Équipement sur site"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__planned_date_begin
msgid "Start Date"
msgstr ""
msgstr "Date de début"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
@ -816,6 +907,7 @@ msgstr "Tâche"
#: model:ir.actions.act_window,name:bemade_fsm.task_template_act_window
#: model:ir.model.fields,field_description:bemade_fsm.field_product_product__task_template_id
#: model:ir.model.fields,field_description:bemade_fsm.field_product_template__task_template_id
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task_from_template_wizard__task_template_id
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_form_view
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_search_view
#: model_terms:ir.ui.view,arch_db:bemade_fsm.task_template_tree_view
@ -835,14 +927,10 @@ msgstr "Nom de tâche"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_tasks_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_timesheet_entries
msgid "Technician"
msgstr "Technicien·ne"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
msgid "Technicians:"
msgstr "Technicien·ne·s:"
#. module: bemade_fsm
#: model:ir.model,name:bemade_fsm.model_project_task_template
msgid "Template for new project tasks"
@ -855,6 +943,11 @@ msgstr ""
"L'équipement à désservir par défaut pour les nouveaux articles de commande "
"de ventes."
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_project_task_from_template_wizard__project_id
msgid "The project the new task should be created in."
msgstr ""
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_bemade_fsm_visit__so_section_id
msgid ""
@ -864,6 +957,20 @@ msgstr ""
"La section sur la commande de vente représentant la main d'oeuvre et les "
"pièces pour cette visite"
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_project_task_from_template_wizard__task_template_id
msgid "The template to use when creating the new task."
msgstr "Le gabarit à utiliser en créant la nouvelle tâche."
#. module: bemade_fsm
#: model:ir.model.fields,help:bemade_fsm.field_project_task_from_template_wizard__new_task_title
msgid ""
"The title (name) for the newly created task. If left blank, the name of the "
"template will be used."
msgstr ""
"Le titre (nom) de la nouvelle tâche. Si laissé vide, le nom du gabarit sera "
"utilisé."
#. module: bemade_fsm
#: model_terms:ir.actions.act_window,help:bemade_fsm.task_template_act_window
msgid "There are no task templates, click above to create one."
@ -871,7 +978,22 @@ msgstr ""
"Il n'existe aucun gabarit de tâche, cliquez ci-dessus pour en créer une."
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: code:addons/bemade_fsm/models/sale_order_line.py:0
#, python-format
msgid ""
"This task has been created from: <a href=# data-oe-model=sale.order data-oe-"
"id=%d>%s</a> (%s)"
msgstr ""
"Cette tâche a été créée à partir de : <a href=# data-oe-model=sale.order data-oe-"
"id=%d>%s</a> (%s)"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_timesheet_entries
msgid "Time"
msgstr "Temps"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "Time &amp; Material"
msgstr "Temps &amp; matériaux"
@ -886,7 +1008,7 @@ msgid "Type of the exception activity on record."
msgstr "Type d'activité d'exception sur l'enregistrement."
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour
msgid "Unit Price"
msgstr "Prix unitaire"
@ -906,13 +1028,19 @@ msgid "View Task"
msgstr "Voir tâche"
#. module: bemade_fsm
#: code:addons/bemade_fsm/models/sale_order_line.py:0
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__visit_id
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order__visit_ids
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__visit_id
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__visit_ids
#, python-format
msgid "Visit"
msgstr "Visite"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_sale_order_line__visit_ids
msgid "Visits"
msgstr "Visites"
#. module: bemade_fsm
#: model:project.task.type,name:bemade_fsm.planning_project_stage_waiting_parts
msgid "Waiting on Parts"
@ -928,24 +1056,23 @@ msgstr "Messages du site web"
msgid "Website communication history"
msgstr "Historique de communication du site web"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_timesheet_entries
msgid "Work Completed"
msgstr "Travaux complétés"
#. module: bemade_fsm
#: model:project.task.type,name:bemade_fsm.planning_project_stage_work_completed
msgid "Work Executed"
msgstr "Travaux exécutés"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
msgid "Work Order"
msgstr "Bon de travail"
#. module: bemade_fsm
#: model_terms:ir.ui.view,arch_db:bemade_fsm.workorder_page_info_block
msgid ""
"Work Order\n"
" Contacts:"
msgstr ""
"Bon de travail\n"
" Contacts :"
"Contacts recevant le bon de travail :"
#. module: bemade_fsm
#: model:ir.model.fields,field_description:bemade_fsm.field_project_task__work_order_contacts

View file

@ -72,4 +72,5 @@ class Equipment(models.Model):
def _compute_complete_name(self):
for rec in self:
tag_part = "[%s] " % rec.pid_tag if rec.pid_tag else ""
rec.complete_name = tag_part + rec.name
name = rec.name or ""
rec.complete_name = tag_part + name

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)
@ -88,10 +94,23 @@ class Task(models.Model):
pattern = re.compile(r"\d+$")
matches = map(lambda n: pattern.search(n), prev_seqs)
seq += max(map(lambda n: int(n.group(1)) if n else 0), matches)
rec.work_order_number = rec.sale_order_id.name.replace('SO', 'WO', 1) \
rec.work_order_number = rec.sale_order_id.name.replace('SO', 'SVR', 1) \
+ f"-{seq}"
return res
def write(self, vals):
super().write(vals)
if not self: # End recursion on empty RecordSet
return
if 'propagate_assignment' in vals:
# When a user sets propagate assignment, it should propagate that setting all the way down the chain
self.child_ids.write({'propagate_assignment': vals['propagate_assignment']})
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 +253,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

@ -1,7 +1,61 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<template id="workorder_page_sale_order_table">
<template id="workorder_page_materials_table">
<t t-set="order_lines"
t-value="doc.relevant_order_lines.filtered(lambda l:
l.product_id.type != 'service' and not l.is_downpayment and not l.visit_id)"/>
<t t-if="order_lines">
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')"/>
<t t-set="section_lines"
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"/>
</t>
<div t-if="order_lines.filtered(lambda l: not l.display_type)" style="page-break-inside: avoid;">
<h2 t-if="order_lines">Materials</h2>
<div t-if="order_lines" class="table-responsive-sm">
<table class="table table-sm o_main_table">
<thead>
<tr>
<th class="text-left">Description</th>
<th class="text-right">Ordered</th>
<th class="text-right">Delivered</th>
</tr>
</thead>
<tbody class="sale_tbody">
<t t-foreach="order_lines" t-as="line">
<t t-set="is_task" t-value="line == doc.sale_line_id"/>
<tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''">
<t t-if="not line.display_type">
<td><span t-field="line.name"/></td>
<td class="text-right">
<span t-field="line.product_uom_qty"/>
<span t-field="line.product_uom"
groups="uom.group_uom"/>
</td>
<td class="text-right">
<span t-field="line.qty_delivered"/>
<span t-field="line.product_uom"
groups="uom.group_uom"/>
</td>
</t>
<t t-if="line.display_type == 'line_section'">
<td colspan="99">
<span t-field="line.name"/>
</td>
</t>
<t t-if="line.display_type == 'line_note'">
<td colspan="99">
<span t-field="line.name"/>
</td>
</t>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</template>
<template id="workorder_page_sale_order_table_with_pricing_and_labour">
<t t-set="order_lines" t-value="doc.relevant_order_lines"/>
<t t-if="order_lines">
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')"/>
@ -164,7 +218,6 @@
</template>
<template id="workorder_page_tasks_table">
<t t-set="interventions" t-value="doc.child_ids"/>
<t t-set="timesheets" t-value="doc.timesheet_ids"/>
<t t-foreach="interventions" t-as="intervention">
<div style="page-break-inside: avoid;">
<h2 t-out="str(intervention_index + 1) + '. ' + intervention.name"/>
@ -182,9 +235,8 @@
<thead>
<tr>
<th width="7%">Status</th>
<th width="30%">Task</th>
<th width="18%">Technician</th>
<th width="45%">Comments</th>
<th width="39%">Task</th>
<th width="54%">Comments</th>
</tr>
</thead>
<tbody>
@ -194,11 +246,6 @@
<td>
<span t-out="task.name"/>
</td>
<td>
<t t-foreach="task.user_ids" t-as="user">
<span t-out="user.name + (', ' if not user_last else '')"/>
</t>
</td>
<td t-out="task.description or ''"/>
</tr>
<tr class="mt-0 pt-0">
@ -207,7 +254,6 @@
<t t-call="bemade_fsm.subtask_list"/>
</td>
<td/>
<td/>
</tr>
</t>
</tbody>
@ -218,20 +264,16 @@
</template>
<template id="workorder_page_info_block">
<h1>Work Order <span t-out="doc.work_order_number"></span></h1>
<div class="row">
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}">
<div t-if="doc.user_ids"><h6>Technicians: </h6></div>
<t t-foreach="doc.user_ids" t-as="user">
<div class="mb-3">
<div t-esc="user" t-options='{
"widget": "contact",
"fields": ["name", "email"]
}'/>
</div>
</t>
</div>
<h1>
<span name="work_order_number" t-out="doc.name"/>
</h1>
<h4 t-if="doc.sale_order_id and doc.sale_order_id.client_order_ref">
Purchase Order: <span
name="po_number"
t-out="doc.sale_order_id.client_order_ref"/>
</h4>
<hr/>
<div class="row" name="address_and_time">
<div t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}">
<t t-if="doc.partner_id">
<div><h6>Customer: </h6></div>
@ -241,8 +283,19 @@
}'/>
</t>
</div>
<div t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
t-if="doc.planned_date_begin or doc.planned_date_end">
<div t-if="doc.planned_date_begin"><h6>Planned start: </h6></div>
<div class="mb-3">
<div t-out="doc.planned_date_begin.strftime('%Y-%m-%d %H:%M')"/>
</div>
<div t-if="doc.planned_date_end"><h6>Planned end: </h6></div>
<div class="mb-3">
<div t-out="doc.planned_date_end.strftime('%Y-%m-%d %H:%M')"/>
</div>
</div>
</div>
<div class="row">
<div class="row" name="site_and_work_order_contacts">
<div t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}">
<div t-if="doc.site_contacts"><h6>Site Contacts: </h6></div>
<t t-foreach="doc.site_contacts" t-as="contact">
@ -267,13 +320,20 @@
</t>
</div>
</div>
<hr/>
<div t-if="not is_html_empty(doc.description)" class="row" name="visit_description">
<div class="col-12">
<span t-out="doc.description" />
</div>
</div>
<hr t-if="not is_html_empty(doc.description)"/>
</template>
<template id="workorder_equipment_summary">
<div t-if="doc.equipment_ids and len(doc.child_ids) > 1"
style="page-break-inside: avoid;">
<h3>Equipment Serviced</h3>
<div class="table-responsive-sm">
<table class="table table-sm o_main_table">
<table class="table table-sm o_main_table overflow-hidden">
<thead>
<tr>
<th>Tag</th>
@ -305,6 +365,47 @@
</div>
</div>
</template>
<template id="workorder_page_timesheet_entries">
<t t-set="timesheets" t-value="doc.timesheet_ids"/>
<div t-if="timesheets" style="page-break-inside: avoid;">
<h1>Time</h1>
<div class="table-responsive-sm">
<table class="table table-sm o_main_table">
<thead>
<tr>
<th width="14%">Date</th>
<th width="30%">Technician</th>
<th width="49%">Work Completed</th>
<th width="7%" class="text-right">Time</th>
</tr>
</thead>
<tbody>
<tr t-foreach="timesheets" t-as="timesheet" class="p-1 m-1">
<td>
<span t-out="timesheet.date.strftime('%Y-%m-%d')"/>
</td>
<td>
<span t-field="timesheet.employee_id.name"/>
</td>
<td>
<span t-field="timesheet.name"/>
</td>
<td>
<span t-esc="timesheet.unit_amount"
t-options="{
'widget': 'duration',
'digital': True,
'unit': 'hour',
'round': 'minute'
}"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template id="workorder_page_signature_block">
<div t-if="doc.worksheet_signature">
<div t-if="report_type == html" class="ribbon" style="
@ -342,7 +443,8 @@
<template id="work_order_page">
<div class="page">
<t t-call="bemade_fsm.workorder_page_info_block"/>
<t t-call="bemade_fsm.workorder_page_sale_order_table"/>
<t t-call="bemade_fsm.workorder_page_timesheet_entries"/>
<t t-call="bemade_fsm.workorder_page_materials_table"/>
<t t-call="bemade_fsm.workorder_equipment_summary"/>
<t t-call="bemade_fsm.workorder_page_signature_block"/>
<t t-call="bemade_fsm.workorder_page_tasks_table"/>
@ -359,13 +461,14 @@
</t>
</t>
</template>
<!-- MD Commented out since this no longer matches the structure of industry_fsm at all -->
<!-- Make the front-end "Sign" page show our new template -->
<template id="portal_my_worksheet"
inherit_id="industry_fsm_report.portal_my_worksheet">
<xpath expr="//div[@t-call='industry_fsm_report.worksheet_custom_page']"
position="replace">
<div t-call="bemade_fsm.work_order_page"/>
</xpath>
</template>
<!-- <template id="portal_my_worksheet" -->
<!-- inherit_id="industry_fsm.portal_my_task"> -->
<!-- <xpath expr="//div[@t-call='industry_fsm.worksheet_custom_page']" -->
<!-- position="replace"> -->
<!-- <div t-call="bemade_fsm.work_order_page"/> -->
<!-- </xpath> -->
<!-- </template> -->
</data>
</odoo>
</odoo>

View file

@ -1,13 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<record id="industry_fsm_report.task_custom_report" model="ir.actions.report">
<record id="industry_fsm.task_custom_report" model="ir.actions.report">
<field name="name">Work Order Report (PDF)</field>
<field name="report_name">bemade_fsm.work_order</field>
<field name="report_file">bemade_fsm.work_order</field>
<field name="print_report_name">'%s Work Order %s' % (
time.strftime('%Y-%m-%d'), object.work_order_number)</field>
<field name="print_report_name">'%s %s' % (
object.planned_date_begin.strftime('%Y-%m-%d') if object.planned_date_begin else time.strftime('%Y-%m-%d'),
object.name
)
</field>
<field name="report_type">qweb-pdf</field>
</record>
</data>
</odoo>
</odoo>

View file

@ -1,146 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="default_worksheet_template" model="ir.actions.report">
<field name="name">Complete Worksheet Report (PDF)</field>
<field name="model">project.task</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">bemade_fsm.worksheet_complete</field>
<field name="print_report_name">
'%s Worksheet %s' % (time.strftime('%Y-%m-%d'), object.name)
</field>
<field name="binding_model_id" ref="model_project_task"/>
<field name="binding_type">report</field>
</record>
<template id="worksheet_complete">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="bemade_fsm.worksheet_complete_page"
t-lang="doc.partner_id.lang"/>
</t>
</t>
</template>
<template id="worksheet_complete_page">
<t t-call="web.external_layout">
<t t-set="address" t-if="doc.sale_order_id">
<strong class="big-red">Customer:</strong>
<div t-field="doc.sale_order_id.partner_id"
t-options='{"widget": "contact",
"fields": ["address", "name"], "no_marker": True}'/>
</t>
<t t-set="information_block">
<strong>Service Address:</strong>
<div t-field="doc.partner_id"
t-options='{"widget": "contact",
"fields": ["address", "name", "phone"],
"no_marker": True, "phone_icons": True}'/>
</t>
<div class="page">
<div class="oe_structure"/>
<h2 class="mt-3">
<span>Work Order </span>
<span t-if="doc.sale_order_id" t-field="doc.sale_order_id.name"/>
<span t-else="" t-field="doc.name"/>
</h2>
<div class="container">
<div class="row mb-3 border-dark">
<div t-if="doc.sale_order_id.client_order_ref" class="col-4">
<span class="font-weight-bolder">Your Reference:</span>
<span t-field="doc.sale_order_id.client_order_ref"/>
</div>
<div t-if="doc.site_contacts" class="col-4">
<span class="font-weight-bolder">Site Contacts:</span>
<t t-foreach="doc.site_contacts" t-as="contact">
<span t-field="contact.name"/>
<span t-out="contact"
t-options='{"widget": "contact",
"fields": ["phone", "mobile", "email"],
"no_marker": True, "phone_icons": True}'/>
</t>
</div>
<div t-if="doc.work_order_contacts" class="col-4">
<span class="font-weight-bolder">Work order attn:</span>
<t t-foreach="doc.work_order_contacts" t-as="contact">
<span t-field="contact.name"/>
<span t-out="contact"
t-options='{"widget": "contact",
"fields": ["phone", "mobile", "email"],
"no_marker": True, "phone_icons": True}'/>
</t>
</div>
</div>
<div class="row mb-3 border-dark">
<div class="col-4" t-if="doc.user_ids">
<span class="font-weight-bolder">Technician(s)</span>
<t t-foreach="doc.user_ids" t-as="technician">
<p class="m-0" t-field="technician.name"/>
</t>
</div>
<div class="col-4" t-if="doc.planned_date_begin">
<span class="font-weight-bolder">Planned Start</span>
<p t-field="doc.planned_date_begin"
t-options="{'widget': 'datetime'}"/>
</div>
<div class="col-4" t-if="doc.planned_date_end">
<span class="font-weight-bolder">Planned End</span>
<p t-field="doc.planned_date_end"
t-options="{'widget': 'datetime'}"/>
</div>
</div>
</div>
<div class="container">
<h3 class="mb32"><strong>Interventions</strong></h3>
<t t-foreach="doc.child_ids" t-as="intervention">
<div class="row">
<div class="col-6">
<h4><span t-field="intervention.name"/></h4>
<span t-if="intervention.description"
t-field="intervention.description"/>
</div>
<div t-if="intervention.equipment_ids"
class="col-6">
<h4>Equipment: </h4>
<t t-foreach="intervention.equipment_ids"
t-as="equipment">
<span t-field="equipment.complete_name"/>
<span t-if="equipment_index &lt; equipment_size -1"
t-out="', '"/>
</t>
</div>
</div>
<div class="row">
<table class="table table-borderless col-12">
<thead>
<th>Done</th>
<th>Task to do</th>
</thead>
<tbody>
<t t-foreach="intervention.child_ids"
t-as="task">
<tr class="wo-task">
<td style="width: 5%">
<input type="checkbox"
t-att-checked="task.is_complete"/>
</td>
<td style="width: 95%">
<p t-field="task.name"/>
<p t-field="task.description"/>
</td>
<!-- TODO: Figure out what to do with the old
concept of task comments -->
</tr>
</t>
</tbody>
</table>
</div>
</t>
</div>
</div>
</t>
</template>
</data>
</odoo>

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

@ -19,3 +19,7 @@ class TestEquipment(BemadeFSMBaseTest):
partner_company.write({'equipment_ids': [Command.set([])]})
with self.assertRaises(MissingError):
equipment.name
def test_compute_complete_name_when_name_blank(self):
equipment = self._generate_equipment(name=False)
complete_name = equipment.complete_name

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,56 @@
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):
@classmethod
def setUpClass(cls):
# Chose to set up all tests the same way since this code was becoming very redundant
super().setUpClass()
cls.so = cls._generate_sale_order()
cls.template = cls._generate_task_template(names=['Parent', 'Child', 'Grandchild'], structure=[2, 1])
cls.product = cls._generate_product(task_template=cls.template)
cls.sol = cls._generate_sale_order_line(sale_order=cls.so, product=cls.product)
cls.user = cls._generate_project_manager_user('Bob', 'Bob')
cls.so.action_confirm()
cls.task = cls.sol.task_id
def test_reassigning_assignment_propagating_task_changes_subtasks(self):
task = self.task
task.propagate_assignment = True
task.write({
'user_ids': [Command.set([self.user.id])],
'propagate_assignment': True,
})
self.assertTrue(all([t.user_ids == self.user for t in task | task._get_all_subtasks()]))
def test_reassigning_task_doesnt_propagate_by_default(self):
task = self.task
task.write({
'user_ids': [Command.set([self.user.id])],
'propagate_assignment': True,
})
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))
def test_unset_propagate_assignment_unsets_for_all_children(self):
task = self.task
# First, set propagation and assign
task.propagate_assignment = True
task.write({
'user_ids': [Command.set([self.user.id])]
})
# Then, unset propagation for the children and re-set assignment
task.child_ids.write({'propagate_assignment': False})
self.assertFalse(any([t.propagate_assignment for t in task._get_all_subtasks()]))
# Then, test that assigning the parent only assigns its children, not its grandchildren
task.write({
'user_ids': [Command.set([])]
})
self.assertTrue(all([not t.user_ids for t in task | task.child_ids]))
self.assertTrue(all([t.user_ids == self.user 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

@ -8,24 +8,26 @@
<field name="inherit_id" ref="sale_project.product_template_form_view_invoice_policy_inherit_sale_project"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='project_id']" position="after">
<field name="task_template_id"
attrs="{'invisible': [('service_tracking', 'not in', ('task_global_project', 'task_in_project'))]}"
domain="[('parent', '=', False)]"/>
<field name="task_template_id"
invisible="service_tracking not in ('task_global_project', 'task_in_project')"
domain="[('parent', '=', False)]"/>
</xpath>
<xpath expr="//field[@name='planning_enabled']" position="after">
<field name="is_field_service"/>
</xpath>
</field>
</record>
<record id="product_search_form_view_inherit_bemade_fsm" model="ir.ui.view">
<field name="name">bemade_fsm.product_search_form_view_inherit_bemade_fsm</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="industry_fsm_sale.product_search_form_view_inherit_fsm_sale"/>
<field name="arch" type="xml">
<xpath expr="//searchpanel//field[@name='categ_id']" position="attributes">
<attribute name="limit">0</attribute>
</xpath>
</field>
</record>
<!-- BV: Did comment that one cause can not match inherit_id and don't understand it use -->
<!-- <record id="product_search_form_view_inherit_bemade_fsm" model="ir.ui.view">-->
<!-- <field name="name">bemade_fsm.product_search_form_view_inherit_bemade_fsm</field>-->
<!-- <field name="model">product.product</field>-->
<!-- <field name="inherit_id" ref="industry_fsm_sale.product_search_form_view_inherit_fsm_sale"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//searchpanel//field[@name='categ_id']" position="attributes">-->
<!-- <attribute name="limit">0</attribute>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
</data>
</odoo>

View file

@ -17,21 +17,22 @@
<page name="field_service" string="Field Service">
<field name="owned_equipment_ids" invisible="True"/>
<group>
<field name="site_contacts"
attrs="{'invisible': [('company_type','=','person')]}"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="work_order_contacts"
attrs="{'invisible': [('company_type','=','person')]}"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="equipment_ids" attrs="{'invisible': [('company_type','=','person')]}"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="False"/>
<field attrs="{'invisible': ['|', ('company_type','=','person'), ('owned_equipment_ids','=',False)]}"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
name="owned_equipment_ids"
readonly="True"/>
<field name="site_ids"
attrs="{'invisible': [('company_type','=','company')]}">
<field name="site_contacts"
invisible="company_type == 'person'"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="work_order_contacts"
invisible="company_type == 'person'"
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"/>
<field name="equipment_ids"
invisible="company_type == 'person'"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="False"/>
<field name="owned_equipment_ids"
invisible="company_type == 'person' or owned_equipment_ids == False"
context="{'tree_view_ref': 'bemade_fsm.fsm_equipment_view_tree'}"
readonly="True"/>
<field name="site_ids"
invisible="company_type == 'company'">
<tree editable="bottom">
<field name="name" widget="res_partner_many2one"/>
</tree>

View file

@ -9,8 +9,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='name']/.." position="before">
<h1 class="d-flex flex-row justify-content-between">
<field name="work_order_number"
attrs="{'invisible': [('work_order_number', '=', False)]}"/>
<field name="work_order_number" invisible="work_order_number == False"/>
</h1>
</xpath>
<xpath expr="//page[@name='extra_info']" position="after">
@ -46,6 +45,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 +170,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': '17.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

@ -4,7 +4,7 @@
<record id="helpdesk_stage_spam" model="helpdesk.stage">
<field name="name">Spam</field>
<field name="sequence">50</field>
<field name="is_close">True</field>
<field name="fold">True</field>
</record>
</data>
</odoo>

View file

@ -6,7 +6,8 @@
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_form"/>
<field name="arch" type="xml">
<header position="inside">
<button name="action_add_blacklist" type="object" string="Add to Blacklist" class="oe_highlight" attrs="{'invisible': [('partner_email', '=', False)]}"/>
<field name="partner_email" invisble="1"/>
<button name="action_add_blacklist" type="object" string="Add to Blacklist" class="oe_highlight" invisible="partner_email == False"/>
</header>
</field>
</record>

View file

@ -18,7 +18,7 @@
#
{
'name': 'Helpdesk One Ticket Per Email',
'version': '15.0.1.0.0',
'version': '17.0.1.0.0',
'summary': 'Restrict ticket creation to a single ticket per email received.',
'category': 'Helpdesk',
'author': 'Bemade Inc.',

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
"name": "Hide Decimal on unit",
"version": "15.0.0.1.1",
"version": "17.0.0.1.1",
"category": "Extra Tools",
'summary': 'Hide decimal on Qty when there is no decimal',
"description": """
@ -14,9 +14,8 @@
'purchase'
],
"data": [
# BV : FOR MIGRATION
# 'views/sale.xml',
# 'views/purchase.xml'
'views/sale.xml',
'views/purchase.xml'
],
"auto_install": False,
"installable": True,

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_purchasequotation_document_strip_decimal" inherit_id="purchase.report_purchasequotation_document">
<xpath expr="//tbody//td[hasclass('text-right')]" position="replace">
<td name="td_quantity" class="text-right">
<xpath expr="//tbody//td[hasclass('text-end')]" position="replace">
<td name="td_quantity" class="text-end">
<t t-if="int(order_line.product_uom_qty) == order_line.product_uom_qty">
<span t-esc="'%.0f' % order_line.product_uom_qty"/>
</t>
@ -15,8 +15,8 @@
</template>
<template id="report_purchaseorder_document_strip_decimal" inherit_id="purchase.report_purchaseorder_document">
<xpath expr="//tbody//td[hasclass('text-right')][1]" position="replace">
<td name="td_quantity" class="text-right">
<xpath expr="//tbody//td[hasclass('text-end')][1]" position="replace">
<td name="td_quantity" class="text-end">
<t t-if="int(line.product_uom_qty) == line.product_uom_qty">
<span t-esc="'%.0f' % line.product_uom_qty"/>
</t>

View file

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

View file

@ -0,0 +1,37 @@
#
# 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': 'Quebec Payroll',
'version': '17.0.1.0.0',
'summary': 'Computations for Quebec Payslips',
'category': 'Human Resources/Payroll',
'author': 'Bemade Inc.',
'website': 'http://www.bemade.org',
'license': 'OPL-1',
'depends': [
'hr_payroll',
'l10n_ca',
],
'data': [
'data/hr_salary_rule_data.xml',
],
'assets': {},
'installable': True,
'auto_install': False
}

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="l10n_ca_input_bonus" model="hr.payslip.input.type">
<field name="name">Bonus or other non-period payment</field>
<field name="code">BONUS</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
<record id="l10n_ca_input_fed_f1" model="hr.payslip.input.type">
<field name="name">Employee-requested deductions (Federal)</field>
<field name="code">FED_F1</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
<record id="l10n_ca_input_fed_f2" model="hr.payslip.input.type">
<field name="name">Court-ordered deductions (Federal)</field>
<field name="code">FED_F2</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
<record id="l10n_ca_input_fed_U1" model="hr.payslip.input.type">
<field name="name">Union or association dues for the period (Federal)</field>
<field name="code">FED_U1</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
<record id="l10n_ca_input_fed_HD" model="hr.payslip.input.type">
<field name="name">Allowance for residents of specified regions (Federal)</field>
<field name="code">FED_HD</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
<record id="l10n_ca_input_fed_F" model="hr.payslip.input.type">
<field name="name">Deduction for retirement plan contributions (Federal)</field>
<field name="code">FED_F</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
<record id="l10n_ca_input_fed_TC" model="hr.payslip.input.type">
<field name="name">Total Requested Amount on Federal form TD1</field>
<field name="code">FED_TC</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
</record>
</data>
</odoo>

View file

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="l10n_ca_parameter_ARK" model="hr.rule.parameter">
<field name="name">A, R, K constants for Federak Tax Calculation</field>
<field name="code">ARK</field>
</record>
<record id="l10n_ca_parameter_value_ARK_2024" model="hr.rule.parameter.value">
<field name="date_from">2024-01-01</field>
<field name="rule_parameter_id" ref="bemade_l10n_ca_payroll.l10n_ca_parameter_ARK"/>
<field name="parameter_value">
[
(0, 0.15, 0),
(55867, 0.2050, 3073),
(111733, 0.26, 9218),
(173205, 0.29, 14414),
(246752, 0.33, 24284),
]
</field>
</record>
<record id="l10n_ca_parameter_TC" model="hr.rule.parameter">
<field name="name">Base amount for federal form TD1</field>
<field name="code">FED_TC</field>
</record>
<record id="l10n_ca_parameter_value_TC_2024" model="hr.rule.parameter.value">
<field name="date_from">2024-01-01</field>
<field name="rule_parameter_id" ref="bemade_l10n_ca_payroll.l10n_ca_parameter_TC"/>
<field name="parameter_value">15704</field>
</record>
<record id="l10n_ca_fed_tax_on_payslip" model="hr.salary.rule">
<field name="name">Income Tax - Federal</field>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="sequence" eval="205"/>
<field name="struct_id" ref="hr_payroll.default_structure"/>
<field name="appears_on_payslip" eval="True"/>
<field name="appears_on_employee_cost_dashboard" eval="False"/>
<field name="appears_on_payroll_report" eval="True"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
"""<![CDATA[
Source: https://www.canada.ca/fr/agence-revenu/services/formulaires-publications/retenues-paie/t4127-formules-calcul-retenues-paie/t4127-jan/t4127-jan-formules-calcul-informatise-retenues-paie.html#toc31
Pour le Québec, calcul en 4 étapes (étapes 1, 2, 3, et 6 du guide ci-dessus)
Étape 1: Calculer "A" - le revenu imposable
Étape 2: Calculer "T3" - l'impôt fédéral de base
Étape 3: Calculer "T1" - l'impôt fédéral annuel à payer
Étape 4: Calculer "T" - L'impôt à payer pour cette paye
Étape 1
A = P x (I - F - F2 - F5A -U1) - HD - F1
P = Nombre de périodes de paie dans l'année
I = Rémunération brute pour la période de paie, excluant les primes, augmentations salariales rétroactives ou autres paiements non-périodiques
F = Retenues pour la période pour un REER, RPA, RPAC ou CR.
F2 = saisie ordonnée par la cour (pension alimentaire, etc.)
F5A = Déductions des cotisations supplémentaires au RRQ pour la période de paie
U1 = Cotisations à un syndicat ou assoc. de fonctionnaires, pour la période de paie
HD = Retenue annuelle accordée aux résidents d'une région visée par le règlement selon formulaire TD1
F1 = Retenues annuelles (frais de garde d'enfants, pensions alimentaires, demandées par l'employé et autorisés par bureau svcs. fiscaux)
PI = gains ouvrant droit à une pension pour la période de paie. Nous assumons ici que c'est égal à la rémunération brute.
B = Prime brute, augmentation de salaire rétroactive, ou autres montants non périodiques
"""
pay_periods_map = {
'annually': 1,
'semi-annually': 2,
'quarterly': 4,
'bi-monthly': 6,
'monthly': 12,
'semi-monthly': 24,
'bi-weekly': 26,
'weekly': 52,
'daily': 365,
}
P = payslip.struct_type_id.default_pay_periods_per_year
I = categories.get("GROSS") - (inputs['BONUS'].amount if 'BONUS' in inputs else 0)
F = inputs['FED_F'].amount if 'FED_F' in inputs else 0
F2 = inputs['FED_F2'].amount if 'FED_F2' in inputs else 0
C = categories.get("COTISATIONS_RRQ", 0)
C2 = categories.get("COTISATIONS_RRQ_2", 0)
F5Q = C * (0.01/0.0640) + C2
PI = categories.get("GROSS")
B = inputs['BONUS'].amount if 'BONUS' in inputs else 0
F5A = F5Q * ((PI - B)/PI)
U1 = inputs['FED_U1'].amount if 'FED_U1' in inputs else 0
HD = inputs['FED_HD'].amount if 'FED_HD' in inputs else 0
F1 = inputs['FED_F1'].amount if 'FED_F1' in inputs else 0
A = P * (I - F - F2 - F5A - U1) - HD - F1
"""
Étape 2 - Calcul de l'impôt fédéral de base (T3)
T3 = (R x A) - K - K1 - K2Q - K3 - K4
R = taux d'imposition fédéral qui s'applique au revenu imposable annuel A
"""
# TODO: Get this into a configuration data structure
ARK = payslip._rule_parameter('ARK')
R, K = payslip._l10n_ca_compute_fed_tax_constants(A, ARK)
TC = inputs['FED_TC'].amount if 'FED_TC' in inputs else payslip._rule_parameter('FED_TC_BASIC')
K1 = 0.15 * TC
PM = payslip._rule_parameter('RRQ_NO_MOIS_TOTAL')
IE = A # Assume that insurables are the gross pay
AE = categories.get("EI_CONTR")
K2Q = ((0.15 * min(P * C * (0.0540/0.0640), 3217.50) * (PM/12)) + (0.15 * min(P * AE, 834.24) + (0.15 * min(P * IE * 0.00494, 464.36))
K3 = inputs['FED_K3'].amount if 'FED_K3' in inputs else 0
CCE = payslip._rule_parameter('FED_CCE') # 1 433 for 2024
K4 = min(0.5 * A, CCE)
T3 = (R * A) - K - K1 - K2Q - K3 - K4
"""
Étape 3 - Formule pour calculer l'impôt fédéral à payer (T1)
T1 = ((T3 - (P x LCF)) - (0.165 * T3)
"""
LCF = min(750, 0.15 * (inputs['DED_CAPITAL_PURCH'] if 'DED_CAPITAL_PURCH' in inputs else 0))
T1 = (T3 - (P * LCF)) - (0.165 * T3)
"""
Étape 6 - Formule pour calculer une estimation des retenus d'impôt fédéral pour la période de paie (T)
T = (T1 / P) / L
L = Retenues d'impôt additionnelles pour la période de paie, demandées par l'employé(e) sur TD1
"""
L = inputs['FED_DEDUCT_REQUEST'].amount if 'FED_DEDUCT_REQUEST' in inputs else 0
T = (T1 / P) / L
result = T
]]>
</field>
</record>
</data>
</odoo>

View file

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

View file

@ -0,0 +1,26 @@
from odoo import models, fields, api
class HrPayrollStructureType(models.Model):
_inherit = 'hr.payroll.structure.type'
default_pay_periods_per_year = fields.Integer(
compute="_compute_pay_periods_per_year",
compute_sudo=True,
)
@api.depends("default_schedule_pay")
def _compute_pay_periods_per_year(self):
pay_periods_map = {
'annually': 1,
'semi-annually': 2,
'quarterly': 4,
'bi-monthly': 6,
'monthly': 12,
'semi-monthly': 24,
'bi-weekly': 26,
'weekly': 52,
'daily': 365,
}
for rec in self:
rec.default_pay_periods_per_year = pay_periods_map.get(rec.default_schedule_pay, False)

View file

@ -0,0 +1,29 @@
from odoo import models, fields, api
class Payslip(models.Model):
_inherit = "hr.payslip"
def _l18n_ca_compute_fed_tax_constants(self, taxable_income: float, coefficients):
"""
Take a table of input coefficients in the form
[
(a, r, k),
...
] where a, r, and k are the threshold, tax rate and federal constants from government tables,
and return the r and k applicable for the given annual taxable income.
:param taxable_income: annual taxable income
:param coefficients: coefficients table to use (get it from rule parameters data, usually)
:return: (r, k) values where r is the tax rate and k is the federal constant to use
"""
R = coefficients[0][1]
K = coefficients[0][2]
# Get the rate and constant by income tier (stop once we reach a tier above the taxable income)
for a, r, k in coefficients:
if taxable_income < a:
return R, K
R = r
K = k
return R, K

View file

@ -34,8 +34,9 @@
],
"assets": {
"web.assets_backend": [
"bemade_mailcow_integration/static/src/js/mailcow.js",
"bemade_mailcow_integration/static/src/xml/mailcow_templates.xml",
# BV: Commented out the following lines to avoid errors when installing the module.
# "bemade_mailcow_integration/static/src/js/mailcow.js",
# "bemade_mailcow_integration/static/src/xml/mailcow_templates.xml",
],
},
'installable': True,

View file

@ -3,9 +3,9 @@
<record id="view_res_config_settings_form_inherit_mailcow" model="ir.ui.view">
<field name="name">res.config.settings.form.inherit.mailcow</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="inherit_id" ref="mail.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="emails" position='after'>
<setting id="email_servers_setting" position='after'>
<div id="mailcow">
<h2>Mailcow Settings</h2>
<div class="row mt16 o_settings_container" name="mailcow_setting">
@ -49,7 +49,7 @@
</div>
</div>
</div>
</div>
</setting>
</field>
</record>
</odoo>

View file

@ -6,7 +6,7 @@
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<field name="email" position="before">
<div id="mailcow_mailcow" attrs="{'invisible': [('id', '!=', False)]}">
<div id="mailcow_mailcow" invisible="id != False">
<label for="mailcow_mailbox" string="Create mailbox on Mailcow"/>
<field name="mailcow_mailbox"/>
</div>

View file

@ -18,7 +18,7 @@
#
{
'name': 'Sales Margin on Vendor Price',
'version': '15.0.0.0.1',
'version': '17.0.0.0.1',
'summary': 'Enables calculation of sales margins based on vendor pricelists.',
'description': """Adds a new method for calculating """,
'author': 'Bemade Inc.',

View file

@ -1 +1,2 @@
from . import sale_order
from . import sale_order_line

View file

@ -21,157 +21,3 @@ class SaleOrder(models.Model):
order.margin_percent_actual = order.amount_untaxed and order.margin_actual / order.amount_untaxed
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
purchase_price_vendor = fields.Float(
compute='_compute_purchase_price_vendor',
string="Vendor Price",
groups="base.group_user",
digits='Product Price'
)
margin_percent_vendor = fields.Float(
string='Margin (%) on Vendor Price',
groups='base.group_user',
group_operator='avg',
compute='_compute_margin_vendor'
)
margin_vendor = fields.Float(
string='Margin on Vendor Price',
groups='base.group_user',
digits='Product Price',
compute='_compute_margin_vendor'
)
purchase_price_actual = fields.Float(
compute="_compute_actual_margins",
digits='Product Price',
groups="base.group_user",
string="Purchase Price"
)
margin_actual = fields.Float(
compute="_compute_actual_margins",
digits='Product Price',
groups="base.group_user",
string="Our Margin"
)
margin_percent_actual = fields.Float(
compute="_compute_actual_margins",
groups="base.group_user",
string="Our Margin (%)"
)
@api.depends(
'purchase_price',
'purchase_price_vendor',
'move_ids.product_id',
'move_ids.product_id.qty_available',
'move_ids.state',
'qty_to_deliver'
)
def _compute_actual_margins(self):
""" We want to use the margin based on average inventory valuation when the
sale order line will be completely fulfilled (or has been fulfilled) from stock.
For product not yet in stock we want to use the vendor price. We can also have
blended calculations (partly on vendor price, partly on stock valuation). This
occurs when an order has been or would be partially fulfilled from available
stock.
:return:
"""
non_product_lines = self.filtered(lambda r: not r.product_id)
non_product_lines.purchase_price_actual = 0.0
non_product_lines.margin_actual = 0.0
non_product_lines.margin_percent_actual = 0.0
for line in self - non_product_lines:
stock_missing = line._determine_missing_stock()
if float_is_zero(stock_missing, precision_rounding=line.product_uom.rounding):
# everything is coming from stock, use inventory valuation
line.purchase_price_actual = line.purchase_price
elif float_compare(line.product_uom_qty, stock_missing,
precision_rounding=line.product_uom.rounding) == 0:
# everything is coming from the vendor, use vendor pricing
line.purchase_price_actual = line.purchase_price_vendor
else:
# we have a mix, use blended pricing
qty_from_stock = line.product_uom_qty - stock_missing
line.purchase_price_actual = \
(stock_missing * line.purchase_price_vendor
+ qty_from_stock * line.purchase_price) \
/ line.product_uom_qty
line.margin_actual = line.price_subtotal - (
line.purchase_price_actual * line.product_uom_qty)
line.margin_percent_actual = line.price_subtotal and line.margin_actual / line.price_subtotal
def _determine_missing_stock(self) -> float:
""" Compute how much stock is missing to meet an order line's demand. In the
case of a quotation line, available stock is checked as if the order were to be
placed immediately.
:return: The quantity missing from available stock to fulfill the line, in
the unit of measure matching self.product_uom.
"""
self.ensure_one()
is_order = self.order_id.state in ('sale', 'done')
if is_order and self.qty_to_deliver == 0:
return 0
elif is_order and self.qty_to_deliver > 0:
reserved = sum([m.reserved_availability for m in self.move_ids])
missing = self.qty_to_deliver - reserved
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
# Not enough reserved, check stock
missing = missing - self.product_id.qty_available
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
# Missing some stock to meet demand, return the quantity
return missing
else:
# Enough stock available to meet this line's demand
return 0
else:
# Already have stock reserved
return 0
else:
# This is a quotation, don't bother with stock reservations
missing = self.product_uom_qty - self.product_id.qty_available
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
return missing
else:
return 0
@api.depends('product_id', 'product_id.seller_ids',
'product_id.seller_ids.price')
def _compute_purchase_price_vendor(self):
for line in self:
product = line.product_id
suppinfos = product.seller_ids.sorted('sequence')
if not suppinfos:
line.purchase_price_vendor = 0.0
continue
suppinfo = suppinfos[0]
purch_currency = suppinfo.currency_id
to_cur = line.currency_id or line.order_id.currency_id
line.purchase_price_vendor = purch_currency._convert(
from_amount=suppinfo.price,
to_currency=to_cur,
company=line.company_id or self.env.company,
date=line.order_id.date_order or fields.Date.today(),
round=False,
) if to_cur and suppinfo.price else suppinfo.price
@api.depends('purchase_price_vendor')
def _compute_margin_vendor(self):
for line in self:
if not line.price_unit or float_is_zero(line.price_unit):
line.margin_vendor = 0
line.margin_percent_vendor = 0
continue
unit_margin = line.price_unit - line.purchase_price_vendor
line.margin_percent_vendor = unit_margin / line.price_unit
line.margin_vendor = unit_margin * line.product_uom_qty

View file

@ -0,0 +1,158 @@
from odoo import models, fields, api, _
from odoo.tools.float_utils import float_is_zero, float_compare
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
purchase_price_vendor = fields.Float(
compute='_compute_purchase_price_vendor',
string="Vendor Price",
groups="base.group_user",
digits='Product Price'
)
margin_percent_vendor = fields.Float(
string='Margin (%) on Vendor Price',
groups='base.group_user',
group_operator='avg',
compute='_compute_margin_vendor'
)
margin_vendor = fields.Float(
string='Margin on Vendor Price',
groups='base.group_user',
digits='Product Price',
compute='_compute_margin_vendor'
)
purchase_price_actual = fields.Float(
compute="_compute_actual_margins",
digits='Product Price',
groups="base.group_user",
string="Purchase Price"
)
margin_actual = fields.Float(
compute="_compute_actual_margins",
digits='Product Price',
groups="base.group_user",
string="Our Margin"
)
margin_percent_actual = fields.Float(
compute="_compute_actual_margins",
groups="base.group_user",
string="Our Margin (%)"
)
@api.depends(
'purchase_price',
'purchase_price_vendor',
'move_ids.product_id',
'move_ids.product_id.qty_available',
'move_ids.state',
'qty_to_deliver'
)
def _compute_actual_margins(self):
""" We want to use the margin based on average inventory valuation when the
sale order line will be completely fulfilled (or has been fulfilled) from stock.
For product not yet in stock we want to use the vendor price. We can also have
blended calculations (partly on vendor price, partly on stock valuation). This
occurs when an order has been or would be partially fulfilled from available
stock.
:return:
"""
non_product_lines = self.filtered(lambda r: not r.product_id)
non_product_lines.purchase_price_actual = 0.0
non_product_lines.margin_actual = 0.0
non_product_lines.margin_percent_actual = 0.0
for line in self - non_product_lines:
stock_missing = line._determine_missing_stock()
if float_is_zero(stock_missing, precision_rounding=line.product_uom.rounding):
# everything is coming from stock, use inventory valuation
line.purchase_price_actual = line.purchase_price
elif float_compare(line.product_uom_qty, stock_missing,
precision_rounding=line.product_uom.rounding) == 0:
# everything is coming from the vendor, use vendor pricing
line.purchase_price_actual = line.purchase_price_vendor
else:
# we have a mix, use blended pricing
qty_from_stock = line.product_uom_qty - stock_missing
line.purchase_price_actual = \
(stock_missing * line.purchase_price_vendor
+ qty_from_stock * line.purchase_price) \
/ line.product_uom_qty
line.margin_actual = line.price_subtotal - (
line.purchase_price_actual * line.product_uom_qty)
line.margin_percent_actual = line.price_subtotal and line.margin_actual / line.price_subtotal
def _determine_missing_stock(self) -> float:
""" Compute how much stock is missing to meet an order line's demand. In the
case of a quotation line, available stock is checked as if the order were to be
placed immediately.
:return: The quantity missing from available stock to fulfill the line, in
the unit of measure matching self.product_uom.
"""
self.ensure_one()
is_order = self.order_id.state in ('sale', 'done')
if is_order and self.qty_to_deliver == 0:
return 0
elif is_order and self.qty_to_deliver > 0:
reserved = sum([m.reserved_availability for m in self.move_ids])
missing = self.qty_to_deliver - reserved
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
# Not enough reserved, check stock
missing = missing - self.product_id.qty_available
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
# Missing some stock to meet demand, return the quantity
return missing
else:
# Enough stock available to meet this line's demand
return 0
else:
# Already have stock reserved
return 0
else:
# This is a quotation, don't bother with stock reservations
missing = self.product_uom_qty - self.product_id.qty_available
if float_compare(missing, 0.0,
precision_rounding=self.product_uom.rounding) == 1:
return missing
else:
return 0
@api.depends('product_id', 'product_id.seller_ids',
'product_id.seller_ids.price')
def _compute_purchase_price_vendor(self):
for line in self:
product = line.product_id
suppinfos = product.seller_ids.sorted('sequence')
if not suppinfos:
line.purchase_price_vendor = 0.0
continue
suppinfo = suppinfos[0]
purch_currency = suppinfo.currency_id
to_cur = line.currency_id or line.order_id.currency_id
line.purchase_price_vendor = purch_currency._convert(
from_amount=suppinfo.price,
to_currency=to_cur,
company=line.company_id or self.env.company,
date=line.order_id.date_order or fields.Date.today(),
round=False,
) if to_cur and suppinfo.price else suppinfo.price
@api.depends('purchase_price_vendor')
def _compute_margin_vendor(self):
for line in self:
if not line.price_unit or float_is_zero(line.price_unit):
line.margin_vendor = 0
line.margin_percent_vendor = 0
continue
unit_margin = line.price_unit - line.purchase_price_vendor
line.margin_percent_vendor = unit_margin / line.price_unit
line.margin_vendor = unit_margin * line.product_uom_qty

View file

@ -1,6 +1,6 @@
{
'name': 'Module Linker',
'version': '15.0.1.0.0',
'version': '17.0.1.0.0',
'category': 'Extra Tools',
'summary': 'Link modules from external repositories',
'description': """

View file

@ -17,22 +17,27 @@
# DEALINGS IN THE SOFTWARE.
#
{
'name': 'bemade_multiple_billing_contacts',
'version': '15.0.1.0.1',
'name': 'Multiple Billing Contacts',
'version': '17.0.1.0.1',
'summary': 'Send invoices to multiple contacts by default.',
'description': """By default, newly created invoices add all invoice addresses for the given partner as
followers on the invoice. If billing contacts are set manually on the sales order, those billing
contacts are added as followers on the invoice instead.""",
'description': """
By default, newly created invoices add all invoice addresses for the given partner as
followers on the invoice. If billing contacts are set manually on the sales order, those billing
contacts are added as followers on the invoice instead.
""",
'category': 'Invoicing Management',
'author': 'Bemade Inc.',
'website': 'https://www.bemade.org',
'license': 'OPL-1',
'depends': ['sale',
'account',
'bemade_partner_root_ancestor',
],
'data': ['views/account_move_views.xml',
'views/res_partner_views.xml'],
'depends': [
'sale',
'account',
'bemade_partner_root_ancestor',
],
'data': [
'views/account_move_views.xml',
'views/res_partner_views.xml'
],
'demo': [],
'installable': True,
'auto_install': False,

View file

@ -4,11 +4,13 @@ from odoo import models, fields, api
class AccountMove(models.Model):
_inherit = 'account.move'
billing_contacts = fields.Many2many(comodel_name='res.partner',
string="Billing Contacts",
compute='_compute_billing_contacts',
inverse='_inverse_billing_contacts',
store=True,)
billing_contacts = fields.Many2many(
comodel_name='res.partner',
string="Billing Contacts",
compute='_compute_billing_contacts',
inverse='_inverse_billing_contacts',
store=True
)
@api.depends('line_ids.sale_line_ids.order_id', 'partner_id')
def _compute_billing_contacts(self):

View file

@ -4,12 +4,17 @@ from odoo import models, fields, api, _, Command
class Partner(models.Model):
_inherit = 'res.partner'
billing_contacts = fields.Many2many(string='Default Billing Contacts',
comodel_name='res.partner',
compute='_compute_billing_contacts',
inverse='_inverse_billing_contacts')
potential_billing_contacts = fields.Many2many(comodel_name='res.partner',
compute='_compute_billing_contacts')
billing_contacts = fields.Many2many(
string='Default Billing Contacts',
comodel_name='res.partner',
compute='_compute_billing_contacts',
inverse='_inverse_billing_contacts'
)
potential_billing_contacts = fields.Many2many(
comodel_name='res.partner',
compute='_compute_billing_contacts'
)
@api.depends('child_ids.type')
def _compute_billing_contacts(self):

View file

@ -4,11 +4,13 @@ from odoo import models, fields, api, _, Command
class SaleOrder(models.Model):
_inherit = 'sale.order'
billing_contacts = fields.Many2many(comodel_name='res.partner',
string='Billing Contacts',
compute='_compute_billing_contacts',
inverse='_inverse_billing_contacts',
store=True)
billing_contacts = fields.Many2many(
comodel_name='res.partner',
string='Billing Contacts',
compute='_compute_billing_contacts',
inverse='_inverse_billing_contacts',
store=True
)
@api.depends('partner_id')
def _compute_billing_contacts(self):

View file

@ -8,8 +8,7 @@
<field name="arch" type="xml">
<field name="partner_id" position="after">
<field name="billing_contacts"
attrs="{'invisible': [('move_type', 'not in',
('out_invoice', 'out_refund'))]}"
invisible="move_type not in ('out_invoice', 'out_refund')"
widget="many2many_checkboxes"
domain="['|', '|', ('id', 'in', billing_contacts),
('parent_id', '=', partner_id),

View file

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<!-- Héritage de la vue formulaire du projet pour ajuster les champs de synchronisation -->
<record id="view_project_project_form_inherit_sync" model="ir.ui.view">
<field name="name">project.project.form.inherit.sync.odoo17</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<!-- Ajout d'un nouveau séparateur pour les champs de synchronisation -->
<group string="Synchronisation Odoo">
<field name="customer_odoo_server"/>
<field name="customer_username" invisible="customer_odoo_server == False"/>
<field name="customer_password" invisible="customer_odoo_server == False"/>
</group>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,10 @@
<record id="cron_sync_projects" model="ir.cron">
<field name="name">Synchroniser les Projets avec Bébé</field>
<field name="model_id" ref="model_project_project"/>
<field name="state">code</field>
<field name="code">model.sync_projects_with_baby()</field>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
</record>

View file

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

View file

@ -0,0 +1,47 @@
from odoo import models, fields, api
class Odoo2OdooSync(models.Model):
_name = 'odoo2odoo.sync'
_description = 'Synchronisation Odoo à Odoo'
model_id = fields.Many2one(
comodel_name='ir.model',
string='Modèle Cible',
required=True,
help="Le modèle Odoo cible de la synchronisation."
)
res_id = fields.Integer(
string='ID Ressource',
required=True,
help="L'ID de la ressource cible dans le modèle spécifié."
)
json_rpc_request = fields.Text(
string='Requête JSON RPC',
required=True,
help="Le corps complet de la requête JSON RPC pour la synchronisation."
)
state = fields.Selection(
selection=[
('draft', 'Brouillon'),
('success', 'Succès'),
('failed', 'Échoué')
],
string='État',
default='draft',
required=True,
help="L'état de la tentative de synchronisation."
)
last_attempt = fields.Datetime(
string='Dernière Tentative',
help="La date et l'heure de la dernière tentative de synchronisation."
)
confirmation = fields.Datetime(
string='Confirmation',
help="La date et l'heure de la confirmation de la synchronisation réussie."
)

View file

@ -0,0 +1,92 @@
from odoo import models, fields, api
class Project(models.Model):
_inherit = 'project.project' # Héritage du modèle projet existant
customer_project_id = fields.Integer(
string="ID Projet Client",
required=False,
help="L'ID du projet dans le système client."
)
customer_odoo_server = fields.Char(string="Serveur Odoo Client")
customer_username = fields.Char(string="Nom d'usager Client")
customer_password = fields.Char(string="Mot de passe Client", invisible=True)
odoo2odoo_sync_ids = fields.One2many(
comodel_name='odoo2odoo.sync',
inverse_name='project_id',
string='Synchronisations'
)
last_push = fields.Datetime(
string="Dernière Tentative de Synchronisation",
compute="_compute_sync_status",
store=True
)
last_push_completed = fields.Datetime(
string="Dernière Synchronisation Réussie",
compute="_compute_sync_status",
store=True
)
last_updated = fields.Datetime(
string="Dernière Mise à Jour par Bébé",
ompute="_compute_sync_status",
store=True
)
odoo2odoo_sync_count = fields.Integer(
string="Nombre de Synchronisations",
compute="_compute_sync_status"
)
odoo2odoo_sync_completed_count = fields.Integer(
string="Nombre de Synchronisations Réussies",
compute="_compute_sync_status"
)
@api.model
def create(self, vals):
# Appel de la méthode super() pour créer le projet dans Odoo.
new_project = super(Project, self).create(vals)
if 'customer_odoo_server' in vals and vals['customer_odoo_server']:
# Simulation de données pour la synchronisation avec le système "bébé".
# Vous adapterez cette partie selon votre logique spécifique de synchronisation.
json_rpc_request = json.dumps({
"jsonrpc": "2.0",
"method": "create_project",
"params": {
"name": new_project.name,
# Ajoutez d'autres paramètres nécessaires pour la création du projet dans le système "bébé".
},
})
# Création d'un enregistrement dans odoo2odoo.sync pour suivre la tentative de synchronisation.
self.env['odoo2odoo.sync'].create({
'model_id': self.env.ref('base.model_project_project').id,
'res_id': new_project.id,
'json_rpc_request': json_rpc_request,
'state': 'draft', # Commencez avec l'état 'draft' pour la nouvelle synchronisation.
})
return new_project
@api.depends('odoo2odoo_sync_ids.state')
def _compute_sync_status(self):
for project in self:
sync_records = self.env['odoo2odoo.sync'].search([
('model_id.model', '=', 'project.project'),
('res_id', '=', project.id),
], order='last_attempt desc')
project.odoo2odoo_sync_count = len(sync_records)
if sync_records:
project.odoo2odoo_sync_completed_count = len(sync_records.filtered(lambda r: r.state == 'success'))
project.last_push = sync_records[0].last_attempt
success_records = sync_records.filtered(lambda r: r.state == 'success')
if success_records:
project.last_push_completed = success_records[0].last_attempt
# Assume that 'last_updated' reflects the last successful pull from 'bébé'
# This requires additional logic to track when updates are received from the bébé system
project.last_updated = success_records[0].confirmation if success_records else False

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
</data>
</odoo>

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
'name': 'Bemade Odoo Partner Scrapper',
'version': '1.0.0',
'version': '17.0.0.0.0',
'category': 'Administration',
'summary': 'Module for scraping partners from odoo.com.',
'description': """
@ -20,14 +20,15 @@
'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",
],
},
# "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",
# ],
# },
'demo': [],
'installable': True,
'application': False,

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

@ -11,7 +11,7 @@
<field name="is_odoo_user"/>Odoo User
<field name="is_odoo_partner"/>Odoo Partner
<br/>
<div attrs="{'invisible': [('is_odoo_partner', '=', False)]}">
<div invisible="is_odoo_partner == False">
<h3><field name="odoo_partner_type"/> Partner</h3>
</div>
</field>
@ -60,10 +60,10 @@
<xpath expr="//kanban" position="inside">
<field name="odoo_partner_type"/>
</xpath>
<xpath expr="//div[@class='oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban']" position="attributes">
<xpath expr="//div[hasclass('oe_kanban_global_click') and hasclass('o_kanban_record_has_image_fill') and hasclass('o_res_partner_kanban')]" position="attributes">
<attribute name="t-attf-class">oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban oe_kanban_color_#{record.color}</attribute>
</xpath>
<xpath expr="//strong[@class='o_kanban_record_title oe_partner_heading']" position="before">
<xpath expr="//strong[hasclass('o_kanban_record_title') and hasclass('oe_partner_heading')]" position="before">
<strong t-if="record.odoo_partner_type.raw_value" class="o_kanban_record_subtitle oe_partner_heading">
<field name="odoo_partner_type"/>
</strong>

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': '17.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

@ -59,7 +59,7 @@ class ChooseDeliveryPackage(models.TransientModel):
# if no package type found, create one
if not delivery_package_type:
delivery_package_type = delivery_package_type.create({
'name': f'Box {vals["width"]}x{vals["height"]}x{vals["length"]}',
'name': f'Box {self.width}x{self.height}x{self.length}',
'width': self.width,
'height': self.height,
'packaging_length': self.length,

View file

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

View file

@ -8,13 +8,13 @@
<field name="partner_to">{{object.id}}</field>
<field name="subject">Select Your Division at {{object.company_id.name}}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0"
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial, sans-serif; color: #454748; width: 100%; border-collapse:separate;">
<table border="0" cellpadding="0" cellspacing="0"
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial, sans-serif; color: #454748; width: 100%; border-collapse:separate;">
<tbody>
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
@ -25,8 +25,8 @@
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tbody>
<tr>
<td valign="top" style="font-size: 13px;">
@ -55,8 +55,8 @@
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tbody>
<tr>
<td valign="middle" align="left">
@ -104,4 +104,3 @@
</record>
</data>
</odoo>

View file

@ -14,4 +14,4 @@
</t>
</template>
</data>
</odoo>
</odoo>

View file

@ -6,15 +6,14 @@
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<field name="company_type" position="after">
<label for="is_subdivision" string="Division" attrs="{'invisible': [('is_company', '=', False)]}"/>
<field name="is_subdivision" attrs="{'invisible': [('is_company', '=', False)]}"/>
<label for="is_subdivision" string="Division" invisible="is_company == False"/>
<field name="is_subdivision" invisible="is_company == False"/>
</field>
<field name="website" position="before">
<field name="email_domain" attrs="{'invisible': [('is_company', '=', False)]}"/>
<field name="email_domain" invisible="is_company == False"/>
</field>
<field name="parent_id" position="attributes">
<attribute name="attrs">{'invisible': [('is_company','=', True),('is_subdivision','=', False)]}
</attribute>
<attribute name="invisible">is_company == True and is_subdivision == False</attribute>
</field>
</field>
</record>

View file

@ -18,7 +18,7 @@
#
{
'name': 'Partner Root Ancestor',
'version': '15.0.1.0.0',
'version': '17.0.1.0.0',
'summary': 'Technical module to add the field root_ancestor to res.partner.',
'category': 'Generic Modules/Base',
'author': 'Bemade Inc.',

View file

@ -4,11 +4,13 @@ from odoo import models, fields, api, _, Command
class Partner(models.Model):
_inherit = 'res.partner'
root_ancestor = fields.Many2one(comodel_name='res.partner',
string='Root Ancestor',
compute='_compute_root_ancestor',
store=True,
recursive=True)
root_ancestor = fields.Many2one(
comodel_name='res.partner',
string='Root Ancestor',
compute='_compute_root_ancestor',
store=True,
recursive=True
)
@api.depends('parent_id', 'parent_id.root_ancestor')
def _compute_root_ancestor(self):

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

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
"name": "Chatter on the Reordering Rules",
"version": "15.0.0.0.1",
"version": "17.0.0.0.1",
"category": "Extra Tools",
'license': 'GPL-3',
'summary': 'Add chatter on the reordering rules',
@ -15,7 +15,7 @@
],
"data": [
# BV : FOR MIGRATION
# 'views/stock_warehouse_orderpoint.xml',
'views/stock_warehouse_orderpoint.xml',
],
"auto_install": False,
"installable": True,

View file

@ -4,7 +4,7 @@
<record id="view_warehouse_orderpoint_tree_editable_config_chatter" model="ir.ui.view">
<field name="name">stock.warehouse.orderpoint.tree.editable.config.chatter</field>
<field name="model">stock.warehouse.orderpoint</field>
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_tree_editable_config"/>
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_tree_editable"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="editable"/>
@ -19,8 +19,8 @@
<field name="arch" type="xml">
<xpath expr="//form/sheet" position="after">
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="message_ids" widget="mail_thread" />
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</xpath>
</field>

View file

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import models

Some files were not shown because too many files have changed in this diff Show more