[MERGE] trunk
bzr revid: sle@openerp.com-20140428162055-9t72vke2kdkhqcwr
This commit is contained in:
commit
003b3fd41e
|
@ -642,7 +642,7 @@ class account_move_line(osv.osv):
|
|||
(_check_date, 'The date of your Journal Entry is not in the defined period! You should change the date or remove this constraint from the journal.', ['date']),
|
||||
(_check_currency, 'The selected account of your Journal Entry forces to provide a secondary currency. You should remove the secondary currency on the account or select a multi-currency view on the journal.', ['currency_id']),
|
||||
(_check_currency_and_amount, "You cannot create journal items with a secondary currency without recording both 'currency' and 'amount currency' field.", ['currency_id','amount_currency']),
|
||||
(_check_currency_amount, 'The amount expressed in the secondary currency must be positive when the journal item is a debit and negative when if it is a credit.', ['amount_currency']),
|
||||
(_check_currency_amount, 'The amount expressed in the secondary currency must be positive when account is debited and negative when account is credited.', ['amount_currency']),
|
||||
(_check_currency_company, "You cannot provide a secondary currency if it is the same than the company one." , ['currency_id']),
|
||||
]
|
||||
|
||||
|
|
|
@ -384,6 +384,7 @@
|
|||
<group expand="0" string="Group By...">
|
||||
<filter string="User" context="{'group_by':'user_id'}" icon="terp-personal"/>
|
||||
<filter string="Type" context="{'group_by':'type'}" icon="terp-stock_symbol-selection"/>
|
||||
<filter string="Company" context="{'group_by':'company_id'}" icon="terp-go-home" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
@ -881,6 +882,7 @@
|
|||
<field name="price_include"/>
|
||||
<field name="description"/>
|
||||
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
|
||||
<field name="type_tax_use" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -891,6 +893,12 @@
|
|||
<search string="Search Taxes">
|
||||
<field name="name" filter_domain="['|', ('name','ilike',self), ('description','ilike',self)]" string="Tax"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<filter string="Sale" domain="[('type_tax_use','=','sale')]" />
|
||||
<filter string="Purchase" domain="[('type_tax_use','=','purchase')]" />
|
||||
<group string="Group By...">
|
||||
<filter string="Company" domain="[]" context="{'group_by':'company_id'}"/>
|
||||
<filter string="Tax Application" domain="[]" context="{'group_by':'type_tax_use'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<page name="sales_purchases" position="after" version="7.0">
|
||||
<page string="Accounting" col="4" name="accounting" attrs="{'invisible': [('is_company','=',False),('parent_id','!=',False)]}">
|
||||
<page string="Accounting" col="4" name="accounting" attrs="{'invisible': [('is_company','=',False),('parent_id','!=',False)]}" groups="account.group_account_invoice">
|
||||
<group>
|
||||
<group>
|
||||
<field name="property_account_position" widget="selection"/>
|
||||
|
@ -127,7 +127,7 @@
|
|||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Accounting" name="accounting_disabled" attrs="{'invisible': ['|',('is_company','=',True),('parent_id','=',False)]}">
|
||||
<page string="Accounting" name="accounting_disabled" attrs="{'invisible': ['|',('is_company','=',True),('parent_id','=',False)]}" groups="account.group_account_invoice">
|
||||
<div>
|
||||
<p>Accounting-related settings are managed on <button name="open_commercial_entity" type="object" string="the parent company" class="oe_link"/></p>
|
||||
</div>
|
||||
|
|
|
@ -259,6 +259,12 @@ class account_config_settings(osv.osv_memory):
|
|||
def onchange_tax_rate(self, cr, uid, ids, rate, context=None):
|
||||
return {'value': {'purchase_tax_rate': rate or False}}
|
||||
|
||||
def onchange_multi_currency(self, cr, uid, ids, group_multi_currency, context=None):
|
||||
res = {}
|
||||
if not group_multi_currency:
|
||||
res['value'] = {'income_currency_exchange_account_id': False, 'expense_currency_exchange_account_id': False}
|
||||
return res
|
||||
|
||||
def onchange_start_date(self, cr, uid, id, start_date):
|
||||
if start_date:
|
||||
start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d")
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
<label for="id" string="Features"/>
|
||||
<div>
|
||||
<div name="group_multi_currency">
|
||||
<field name="group_multi_currency" class="oe_inline"/>
|
||||
<field name="group_multi_currency" class="oe_inline" on_change="onchange_multi_currency(group_multi_currency)"/>
|
||||
<label for="group_multi_currency"/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# Slovak translation for openobject-addons
|
||||
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
|
||||
# This file is distributed under the same license as the openobject-addons package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: openobject-addons\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2012-12-21 17:05+0000\n"
|
||||
"PO-Revision-Date: 2014-04-26 16:04+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Slovak <sk@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2014-04-27 05:58+0000\n"
|
||||
"X-Generator: Launchpad (build 16985)\n"
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: model:ir.model,name:account_anglo_saxon.model_product_category
|
||||
msgid "Product Category"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: model:ir.model,name:account_anglo_saxon.model_account_invoice_line
|
||||
msgid "Invoice Line"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: model:ir.model,name:account_anglo_saxon.model_purchase_order
|
||||
msgid "Purchase Order"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: model:ir.model,name:account_anglo_saxon.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: field:product.category,property_account_creditor_price_difference_categ:0
|
||||
#: field:product.template,property_account_creditor_price_difference:0
|
||||
msgid "Price Difference Account"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: model:ir.model,name:account_anglo_saxon.model_account_invoice
|
||||
msgid "Invoice"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: model:ir.model,name:account_anglo_saxon.model_stock_picking
|
||||
msgid "Picking List"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_anglo_saxon
|
||||
#: help:product.category,property_account_creditor_price_difference_categ:0
|
||||
#: help:product.template,property_account_creditor_price_difference:0
|
||||
msgid ""
|
||||
"This account will be used to value price difference between purchase price "
|
||||
"and cost price."
|
||||
msgstr ""
|
|
@ -189,7 +189,7 @@ class account_voucher(osv.osv):
|
|||
if not ids:
|
||||
return []
|
||||
if context is None: context = {}
|
||||
return [(r['id'], (str("%.2f" % r['amount']) or '')) for r in self.read(cr, uid, ids, ['amount'], context, load='_classic_write')]
|
||||
return [(r['id'], (r['number'] or _('Voucher'))) for r in self.read(cr, uid, ids, ['number'], context, load='_classic_write')]
|
||||
|
||||
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
|
||||
mod_obj = self.pool.get('ir.model.data')
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
# Slovak translation for openobject-addons
|
||||
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
|
||||
# This file is distributed under the same license as the openobject-addons package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: openobject-addons\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
|
||||
"PO-Revision-Date: 2014-04-26 16:22+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Slovak <sk@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2014-04-27 05:58+0000\n"
|
||||
"X-Generator: Launchpad (build 16985)\n"
|
||||
|
||||
#. module: association
|
||||
#: field:profile.association.config.install_modules_wizard,wiki:0
|
||||
msgid "Wiki"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
msgid "Event Management"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: field:profile.association.config.install_modules_wizard,project_gtd:0
|
||||
msgid "Getting Things Done"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: model:ir.module.module,description:association.module_meta_information
|
||||
msgid "This module is to create Profile for Associates"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: field:profile.association.config.install_modules_wizard,progress:0
|
||||
msgid "Configuration Progress"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
msgid ""
|
||||
"Here are specific applications related to the Association Profile you "
|
||||
"selected."
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: help:profile.association.config.install_modules_wizard,event_project:0
|
||||
msgid "Helps you to manage and organize your events."
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: field:profile.association.config.install_modules_wizard,config_logo:0
|
||||
msgid "Image"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: help:profile.association.config.install_modules_wizard,hr_expense:0
|
||||
msgid ""
|
||||
"Tracks and manages employee expenses, and can automatically re-invoice "
|
||||
"clients if the expenses are project-related."
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: help:profile.association.config.install_modules_wizard,project_gtd:0
|
||||
msgid ""
|
||||
"GTD is a methodology to efficiently organise yourself and your tasks. This "
|
||||
"module fully integrates GTD principle with OpenERP's project management."
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
msgid "Resources Management"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: model:ir.module.module,shortdesc:association.module_meta_information
|
||||
msgid "Association profile"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: field:profile.association.config.install_modules_wizard,hr_expense:0
|
||||
msgid "Expenses Tracking"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: model:ir.actions.act_window,name:association.action_config_install_module
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
msgid "Association Application Configuration"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: help:profile.association.config.install_modules_wizard,wiki:0
|
||||
msgid ""
|
||||
"Lets you create wiki pages and page groups in order to keep track of "
|
||||
"business knowledge and share it with and between your employees."
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: help:profile.association.config.install_modules_wizard,project:0
|
||||
msgid ""
|
||||
"Helps you manage your projects and tasks by tracking them, generating "
|
||||
"plannings, etc..."
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: model:ir.model,name:association.model_profile_association_config_install_modules_wizard
|
||||
msgid "profile.association.config.install_modules_wizard"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: field:profile.association.config.install_modules_wizard,event_project:0
|
||||
msgid "Events"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
#: field:profile.association.config.install_modules_wizard,project:0
|
||||
msgid "Project Management"
|
||||
msgstr ""
|
||||
|
||||
#. module: association
|
||||
#: view:profile.association.config.install_modules_wizard:0
|
||||
msgid "Configure"
|
||||
msgstr ""
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import audittrail
|
||||
import wizard
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
{
|
||||
'name': 'Audit Trail',
|
||||
'version': '1.0',
|
||||
'category': 'Tools',
|
||||
'description': """
|
||||
This module lets administrator track every user operation on all the objects of the system.
|
||||
===========================================================================================
|
||||
|
||||
The administrator can subscribe to rules for read, write and delete on objects
|
||||
and can check logs.
|
||||
""",
|
||||
'author': 'OpenERP SA',
|
||||
'website': 'http://www.openerp.com',
|
||||
'depends': ['base'],
|
||||
'data': [
|
||||
'wizard/audittrail_view_log_view.xml',
|
||||
'audittrail_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'demo': ['audittrail_demo.xml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'images': ['images/audittrail1.jpeg','images/audittrail2.jpeg','images/audittrail3.jpeg'],
|
||||
}
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,538 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import openerp
|
||||
from openerp.osv import fields, osv
|
||||
import openerp.service.model
|
||||
from openerp.tools.translate import _
|
||||
import time
|
||||
from openerp import tools
|
||||
from openerp import SUPERUSER_ID
|
||||
|
||||
class audittrail_rule(osv.osv):
|
||||
"""
|
||||
For Auddittrail Rule
|
||||
"""
|
||||
_name = 'audittrail.rule'
|
||||
_description = "Audittrail Rule"
|
||||
_columns = {
|
||||
"name": fields.char("Rule Name", size=32, required=True),
|
||||
"object_id": fields.many2one('ir.model', 'Object', required=True, help="Select object for which you want to generate log."),
|
||||
"user_id": fields.many2many('res.users', 'audittail_rules_users',
|
||||
'user_id', 'rule_id', 'Users', help="if User is not added then it will applicable for all users"),
|
||||
"log_read": fields.boolean("Log Reads", help="Select this if you want to keep track of read/open on any record of the object of this rule"),
|
||||
"log_write": fields.boolean("Log Writes", help="Select this if you want to keep track of modification on any record of the object of this rule"),
|
||||
"log_unlink": fields.boolean("Log Deletes", help="Select this if you want to keep track of deletion on any record of the object of this rule"),
|
||||
"log_create": fields.boolean("Log Creates",help="Select this if you want to keep track of creation on any record of the object of this rule"),
|
||||
"log_action": fields.boolean("Log Action",help="Select this if you want to keep track of actions on the object of this rule"),
|
||||
"log_workflow": fields.boolean("Log Workflow",help="Select this if you want to keep track of workflow on any record of the object of this rule"),
|
||||
"state": fields.selection((("draft", "Draft"), ("subscribed", "Subscribed")), "Status", required=True),
|
||||
"action_id": fields.many2one('ir.actions.act_window', "Action ID"),
|
||||
}
|
||||
_defaults = {
|
||||
'state': 'draft',
|
||||
'log_create': 1,
|
||||
'log_unlink': 1,
|
||||
'log_write': 1,
|
||||
}
|
||||
_sql_constraints = [
|
||||
('model_uniq', 'unique (object_id)', """There is already a rule defined on this object\n You cannot define another: please edit the existing one.""")
|
||||
]
|
||||
__functions = {}
|
||||
|
||||
def subscribe(self, cr, uid, ids, *args):
|
||||
"""
|
||||
Subscribe Rule for auditing changes on object and apply shortcut for logs on that object.
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param ids: List of Auddittrail Rule’s IDs.
|
||||
@return: True
|
||||
"""
|
||||
obj_action = self.pool.get('ir.actions.act_window')
|
||||
obj_model = self.pool.get('ir.model.data')
|
||||
#start Loop
|
||||
for thisrule in self.browse(cr, uid, ids):
|
||||
if thisrule.object_id.model not in self.pool:
|
||||
raise osv.except_osv(
|
||||
_('WARNING: audittrail is not part of the pool'),
|
||||
_('Change audittrail depends -- Setting rule as DRAFT'))
|
||||
self.write(cr, uid, [thisrule.id], {"state": "draft"})
|
||||
val = {
|
||||
"name": 'View Log',
|
||||
"res_model": 'audittrail.log',
|
||||
"src_model": thisrule.object_id.model,
|
||||
"domain": "[('object_id','=', " + str(thisrule.object_id.id) + "), ('res_id', '=', active_id)]"
|
||||
|
||||
}
|
||||
action_id = obj_action.create(cr, SUPERUSER_ID, val)
|
||||
self.write(cr, uid, [thisrule.id], {"state": "subscribed", "action_id": action_id})
|
||||
keyword = 'client_action_relate'
|
||||
value = 'ir.actions.act_window,' + str(action_id)
|
||||
res = obj_model.ir_set(cr, SUPERUSER_ID, 'action', keyword, 'View_log_' + thisrule.object_id.model, [thisrule.object_id.model], value, replace=True, isobject=True, xml_id=False)
|
||||
#End Loop
|
||||
return True
|
||||
|
||||
def unsubscribe(self, cr, uid, ids, *args):
|
||||
"""
|
||||
Unsubscribe Auditing Rule on object
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param ids: List of Auddittrail Rule’s IDs.
|
||||
@return: True
|
||||
"""
|
||||
obj_action = self.pool.get('ir.actions.act_window')
|
||||
ir_values_obj = self.pool.get('ir.values')
|
||||
value=''
|
||||
#start Loop
|
||||
for thisrule in self.browse(cr, uid, ids):
|
||||
if thisrule.id in self.__functions:
|
||||
for function in self.__functions[thisrule.id]:
|
||||
setattr(function[0], function[1], function[2])
|
||||
w_id = obj_action.search(cr, uid, [('name', '=', 'View Log'), ('res_model', '=', 'audittrail.log'), ('src_model', '=', thisrule.object_id.model)])
|
||||
if w_id:
|
||||
obj_action.unlink(cr, SUPERUSER_ID, w_id)
|
||||
value = "ir.actions.act_window" + ',' + str(w_id[0])
|
||||
val_id = ir_values_obj.search(cr, uid, [('model', '=', thisrule.object_id.model), ('value', '=', value)])
|
||||
if val_id:
|
||||
res = ir_values_obj.unlink(cr, uid, [val_id[0]])
|
||||
self.write(cr, uid, [thisrule.id], {"state": "draft"})
|
||||
#End Loop
|
||||
return True
|
||||
|
||||
class audittrail_log(osv.osv):
|
||||
"""
|
||||
For Audittrail Log
|
||||
"""
|
||||
_name = 'audittrail.log'
|
||||
_description = "Audittrail Log"
|
||||
|
||||
def _name_get_resname(self, cr, uid, ids, *args):
|
||||
data = {}
|
||||
for resname in self.browse(cr, uid, ids,[]):
|
||||
model_object = resname.object_id
|
||||
res_id = resname.res_id
|
||||
if model_object and res_id:
|
||||
model_pool = self.pool[model_object.model]
|
||||
res = model_pool.read(cr, uid, res_id, ['name'])
|
||||
data[resname.id] = res['name']
|
||||
else:
|
||||
data[resname.id] = False
|
||||
return data
|
||||
|
||||
_columns = {
|
||||
"name": fields.char("Resource Name",size=64),
|
||||
"object_id": fields.many2one('ir.model', 'Object'),
|
||||
"user_id": fields.many2one('res.users', 'User'),
|
||||
"method": fields.char("Method", size=64),
|
||||
"timestamp": fields.datetime("Date"),
|
||||
"res_id": fields.integer('Resource Id'),
|
||||
"line_ids": fields.one2many('audittrail.log.line', 'log_id', 'Log lines'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
"timestamp": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
_order = "timestamp desc"
|
||||
|
||||
class audittrail_log_line(osv.osv):
|
||||
"""
|
||||
Audittrail Log Line.
|
||||
"""
|
||||
_name = 'audittrail.log.line'
|
||||
_description = "Log Line"
|
||||
_columns = {
|
||||
'field_id': fields.many2one('ir.model.fields', 'Fields', required=True),
|
||||
'log_id': fields.many2one('audittrail.log', 'Log'),
|
||||
'log': fields.integer("Log ID"),
|
||||
'old_value': fields.text("Old Value"),
|
||||
'new_value': fields.text("New Value"),
|
||||
'old_value_text': fields.text('Old value Text'),
|
||||
'new_value_text': fields.text('New value Text'),
|
||||
'field_description': fields.char('Field Description', size=64),
|
||||
}
|
||||
|
||||
# Monkeypatch the model RPC endpoint for auditing changes.
|
||||
|
||||
def get_value_text(cr, uid, pool, resource_pool, method, field, value):
|
||||
"""
|
||||
Gets textual values for the fields.
|
||||
If the field is a many2one, it returns the name.
|
||||
If it's a one2many or a many2many, it returns a list of name.
|
||||
In other cases, it just returns the value.
|
||||
:param cr: the current row, from the database cursor,
|
||||
:param uid: the current user’s ID for security checks,
|
||||
:param pool: current db's pooler object.
|
||||
:param resource_pool: pooler object of the model which values are being changed.
|
||||
:param field: for which the text value is to be returned.
|
||||
:param value: value of the field.
|
||||
:param recursive: True or False, True will repeat the process recursively
|
||||
:return: string value or a list of values(for O2M/M2M)
|
||||
"""
|
||||
|
||||
field_obj = (resource_pool._all_columns.get(field)).column
|
||||
if field_obj._type in ('one2many','many2many'):
|
||||
data = pool[field_obj._obj].name_get(cr, uid, value)
|
||||
#return the modifications on x2many fields as a list of names
|
||||
res = map(lambda x:x[1], data)
|
||||
elif field_obj._type == 'many2one':
|
||||
#return the modifications on a many2one field as its value returned by name_get()
|
||||
res = value and value[1] or value
|
||||
else:
|
||||
res = value
|
||||
return res
|
||||
|
||||
def create_log_line(cr, uid, log_id, model, lines=None):
|
||||
"""
|
||||
Creates lines for changed fields with its old and new values
|
||||
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param model: Object which values are being changed
|
||||
@param lines: List of values for line is to be created
|
||||
"""
|
||||
if lines is None:
|
||||
lines = []
|
||||
pool = openerp.registry(cr.dbname)
|
||||
obj_pool = pool[model.model]
|
||||
model_pool = pool.get('ir.model')
|
||||
field_pool = pool.get('ir.model.fields')
|
||||
log_line_pool = pool.get('audittrail.log.line')
|
||||
for line in lines:
|
||||
field_obj = obj_pool._all_columns.get(line['name'])
|
||||
assert field_obj, _("'%s' field does not exist in '%s' model" %(line['name'], model.model))
|
||||
field_obj = field_obj.column
|
||||
old_value = line.get('old_value', '')
|
||||
new_value = line.get('new_value', '')
|
||||
search_models = [model.id]
|
||||
if obj_pool._inherits:
|
||||
search_models += model_pool.search(cr, uid, [('model', 'in', obj_pool._inherits.keys())])
|
||||
field_id = field_pool.search(cr, uid, [('name', '=', line['name']), ('model_id', 'in', search_models)])
|
||||
if field_obj._type == 'many2one':
|
||||
old_value = old_value and old_value[0] or old_value
|
||||
new_value = new_value and new_value[0] or new_value
|
||||
vals = {
|
||||
"log_id": log_id,
|
||||
"field_id": field_id and field_id[0] or False,
|
||||
"old_value": old_value,
|
||||
"new_value": new_value,
|
||||
"old_value_text": line.get('old_value_text', ''),
|
||||
"new_value_text": line.get('new_value_text', ''),
|
||||
"field_description": field_obj.string
|
||||
}
|
||||
line_id = log_line_pool.create(cr, uid, vals)
|
||||
return True
|
||||
|
||||
def log_fct(cr, uid_orig, model, method, fct_src, *args, **kw):
|
||||
"""
|
||||
Logging function: This function is performing the logging operation
|
||||
@param model: Object whose values are being changed
|
||||
@param method: method to log: create, read, write, unlink, action or workflow action
|
||||
@param fct_src: execute method of Object proxy
|
||||
|
||||
@return: Returns result as per method of Object proxy
|
||||
"""
|
||||
pool = openerp.registry(cr.dbname)
|
||||
resource_pool = pool[model]
|
||||
model_pool = pool.get('ir.model')
|
||||
model_ids = model_pool.search(cr, SUPERUSER_ID, [('model', '=', model)])
|
||||
model_id = model_ids and model_ids[0] or False
|
||||
assert model_id, _("'%s' Model does not exist..." %(model))
|
||||
model = model_pool.browse(cr, SUPERUSER_ID, model_id)
|
||||
|
||||
# fields to log. currently only used by log on read()
|
||||
field_list = []
|
||||
old_values = new_values = {}
|
||||
|
||||
if method == 'create':
|
||||
res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
|
||||
if res:
|
||||
res_ids = [res]
|
||||
new_values = get_data(cr, uid_orig, pool, res_ids, model, method)
|
||||
elif method == 'read':
|
||||
res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
|
||||
if isinstance(res, dict):
|
||||
records = [res]
|
||||
else:
|
||||
records = res
|
||||
# build the res_ids and the old_values dict. Here we don't use get_data() to
|
||||
# avoid performing an additional read()
|
||||
res_ids = []
|
||||
for record in records:
|
||||
res_ids.append(record['id'])
|
||||
old_values[(model.id, record['id'])] = {'value': record, 'text': record}
|
||||
# log only the fields read
|
||||
field_list = args[1]
|
||||
elif method == 'unlink':
|
||||
res_ids = args[0]
|
||||
old_values = get_data(cr, uid_orig, pool, res_ids, model, method)
|
||||
# process_data first as fct_src will unlink the record
|
||||
self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
|
||||
return fct_src(cr, uid_orig, model.model, method, *args, **kw)
|
||||
else: # method is write, action or workflow action
|
||||
res_ids = []
|
||||
if args:
|
||||
res_ids = args[0]
|
||||
if isinstance(res_ids, (long, int)):
|
||||
res_ids = [res_ids]
|
||||
if res_ids:
|
||||
# store the old values into a dictionary
|
||||
old_values = get_data(cr, uid_orig, pool, res_ids, model, method)
|
||||
# process the original function, workflow trigger...
|
||||
res = fct_src(cr, uid_orig, model.model, method, *args, **kw)
|
||||
if method == 'copy':
|
||||
res_ids = [res]
|
||||
if res_ids:
|
||||
# check the new values and store them into a dictionary
|
||||
new_values = get_data(cr, uid_orig, pool, res_ids, model, method)
|
||||
# compare the old and new values and create audittrail log if needed
|
||||
process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list)
|
||||
return res
|
||||
|
||||
def get_data(cr, uid, pool, res_ids, model, method):
|
||||
"""
|
||||
This function simply read all the fields of the given res_ids, and also recurisvely on
|
||||
all records of a x2m fields read that need to be logged. Then it returns the result in
|
||||
convenient structure that will be used as comparison basis.
|
||||
|
||||
:param cr: the current row, from the database cursor,
|
||||
:param uid: the current user’s ID. This parameter is currently not used as every
|
||||
operation to get data is made as super admin. Though, it could be usefull later.
|
||||
:param pool: current db's pooler object.
|
||||
:param res_ids: Id's of resource to be logged/compared.
|
||||
:param model: Object whose values are being changed
|
||||
:param method: method to log: create, read, unlink, write, actions, workflow actions
|
||||
:return: dict mapping a tuple (model_id, resource_id) with its value and textual value
|
||||
{ (model_id, resource_id): { 'value': ...
|
||||
'textual_value': ...
|
||||
},
|
||||
}
|
||||
"""
|
||||
data = {}
|
||||
resource_pool = pool[model.model]
|
||||
# read all the fields of the given resources in super admin mode
|
||||
for resource in resource_pool.read(cr, SUPERUSER_ID, res_ids, resource_pool._all_columns):
|
||||
values = {}
|
||||
values_text = {}
|
||||
resource_id = resource['id']
|
||||
# loop on each field on the res_ids we just have read
|
||||
for field in resource:
|
||||
if field in ('__last_update', 'id'):
|
||||
continue
|
||||
values[field] = resource[field]
|
||||
# get the textual value of that field for this record
|
||||
values_text[field] = get_value_text(cr, SUPERUSER_ID, pool, resource_pool, method, field, resource[field])
|
||||
|
||||
field_obj = resource_pool._all_columns.get(field).column
|
||||
if field_obj._type in ('one2many','many2many'):
|
||||
# check if an audittrail rule apply in super admin mode
|
||||
if check_rules(cr, SUPERUSER_ID, field_obj._obj, method):
|
||||
# check if the model associated to a *2m field exists, in super admin mode
|
||||
x2m_model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', field_obj._obj)])
|
||||
x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
|
||||
assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
|
||||
x2m_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, x2m_model_id)
|
||||
field_resource_ids = list(set(resource[field]))
|
||||
if model.model == x2m_model.model:
|
||||
# we need to remove current resource_id from the many2many to prevent an infinit loop
|
||||
if resource_id in field_resource_ids:
|
||||
field_resource_ids.remove(resource_id)
|
||||
data.update(get_data(cr, SUPERUSER_ID, pool, field_resource_ids, x2m_model, method))
|
||||
|
||||
data[(model.id, resource_id)] = {'text':values_text, 'value': values}
|
||||
return data
|
||||
|
||||
def prepare_audittrail_log_line(cr, uid, pool, model, resource_id, method, old_values, new_values, field_list=None):
|
||||
"""
|
||||
This function compares the old data (i.e before the method was executed) and the new data
|
||||
(after the method was executed) and returns a structure with all the needed information to
|
||||
log those differences.
|
||||
|
||||
:param cr: the current row, from the database cursor,
|
||||
:param uid: the current user’s ID. This parameter is currently not used as every
|
||||
operation to get data is made as super admin. Though, it could be usefull later.
|
||||
:param pool: current db's pooler object.
|
||||
:param model: model object which values are being changed
|
||||
:param resource_id: ID of record to which values are being changed
|
||||
:param method: method to log: create, read, unlink, write, actions, workflow actions
|
||||
:param old_values: dict of values read before execution of the method
|
||||
:param new_values: dict of values read after execution of the method
|
||||
:param field_list: optional argument containing the list of fields to log. Currently only
|
||||
used when performing a read, it could be usefull later on if we want to log the write
|
||||
on specific fields only.
|
||||
|
||||
:return: dictionary with
|
||||
* keys: tuples build as ID of model object to log and ID of resource to log
|
||||
* values: list of all the changes in field values for this couple (model, resource)
|
||||
return {
|
||||
(model.id, resource_id): []
|
||||
}
|
||||
|
||||
The reason why the structure returned is build as above is because when modifying an existing
|
||||
record, we may have to log a change done in a x2many field of that object
|
||||
"""
|
||||
if field_list is None:
|
||||
field_list = []
|
||||
key = (model.id, resource_id)
|
||||
lines = {
|
||||
key: []
|
||||
}
|
||||
# loop on all the fields
|
||||
for field_name, field_definition in pool[model.model]._all_columns.items():
|
||||
if field_name in ('__last_update', 'id'):
|
||||
continue
|
||||
#if the field_list param is given, skip all the fields not in that list
|
||||
if field_list and field_name not in field_list:
|
||||
continue
|
||||
field_obj = field_definition.column
|
||||
if field_obj._type in ('one2many','many2many'):
|
||||
# checking if an audittrail rule apply in super admin mode
|
||||
if check_rules(cr, SUPERUSER_ID, field_obj._obj, method):
|
||||
# checking if the model associated to a *2m field exists, in super admin mode
|
||||
x2m_model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', field_obj._obj)])
|
||||
x2m_model_id = x2m_model_ids and x2m_model_ids[0] or False
|
||||
assert x2m_model_id, _("'%s' Model does not exist..." %(field_obj._obj))
|
||||
x2m_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, x2m_model_id)
|
||||
# the resource_ids that need to be checked are the sum of both old and previous values (because we
|
||||
# need to log also creation or deletion in those lists).
|
||||
x2m_old_values_ids = old_values.get(key, {'value': {}})['value'].get(field_name, [])
|
||||
x2m_new_values_ids = new_values.get(key, {'value': {}})['value'].get(field_name, [])
|
||||
# We use list(set(...)) to remove duplicates.
|
||||
res_ids = list(set(x2m_old_values_ids + x2m_new_values_ids))
|
||||
if model.model == x2m_model.model:
|
||||
# we need to remove current resource_id from the many2many to prevent an infinit loop
|
||||
if resource_id in res_ids:
|
||||
res_ids.remove(resource_id)
|
||||
for res_id in res_ids:
|
||||
lines.update(prepare_audittrail_log_line(cr, SUPERUSER_ID, pool, x2m_model, res_id, method, old_values, new_values, field_list))
|
||||
# if the value value is different than the old value: record the change
|
||||
if key not in old_values or key not in new_values or old_values[key]['value'][field_name] != new_values[key]['value'][field_name]:
|
||||
data = {
|
||||
'name': field_name,
|
||||
'new_value': key in new_values and new_values[key]['value'].get(field_name),
|
||||
'old_value': key in old_values and old_values[key]['value'].get(field_name),
|
||||
'new_value_text': key in new_values and new_values[key]['text'].get(field_name),
|
||||
'old_value_text': key in old_values and old_values[key]['text'].get(field_name)
|
||||
}
|
||||
lines[key].append(data)
|
||||
return lines
|
||||
|
||||
def process_data(cr, uid, pool, res_ids, model, method, old_values=None, new_values=None, field_list=None):
|
||||
"""
|
||||
This function processes and iterates recursively to log the difference between the old
|
||||
data (i.e before the method was executed) and the new data and creates audittrail log
|
||||
accordingly.
|
||||
|
||||
:param cr: the current row, from the database cursor,
|
||||
:param uid: the current user’s ID,
|
||||
:param pool: current db's pooler object.
|
||||
:param res_ids: Id's of resource to be logged/compared.
|
||||
:param model: model object which values are being changed
|
||||
:param method: method to log: create, read, unlink, write, actions, workflow actions
|
||||
:param old_values: dict of values read before execution of the method
|
||||
:param new_values: dict of values read after execution of the method
|
||||
:param field_list: optional argument containing the list of fields to log. Currently only
|
||||
used when performing a read, it could be usefull later on if we want to log the write
|
||||
on specific fields only.
|
||||
:return: True
|
||||
"""
|
||||
if field_list is None:
|
||||
field_list = []
|
||||
# loop on all the given ids
|
||||
for res_id in res_ids:
|
||||
# compare old and new values and get audittrail log lines accordingly
|
||||
lines = prepare_audittrail_log_line(cr, uid, pool, model, res_id, method, old_values, new_values, field_list)
|
||||
|
||||
# if at least one modification has been found
|
||||
for model_id, resource_id in lines:
|
||||
line_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, model_id).model
|
||||
|
||||
vals = {
|
||||
'method': method,
|
||||
'object_id': model_id,
|
||||
'user_id': uid,
|
||||
'res_id': resource_id,
|
||||
}
|
||||
if (model_id, resource_id) not in old_values and method not in ('copy', 'read'):
|
||||
# the resource was not existing so we are forcing the method to 'create'
|
||||
# (because it could also come with the value 'write' if we are creating
|
||||
# new record through a one2many field)
|
||||
vals.update({'method': 'create'})
|
||||
if (model_id, resource_id) not in new_values and method not in ('copy', 'read'):
|
||||
# the resource is not existing anymore so we are forcing the method to 'unlink'
|
||||
# (because it could also come with the value 'write' if we are deleting the
|
||||
# record through a one2many field)
|
||||
name = old_values[(model_id, resource_id)]['value'].get('name',False)
|
||||
vals.update({'method': 'unlink'})
|
||||
else :
|
||||
name = pool[line_model].name_get(cr, uid, [resource_id])[0][1]
|
||||
vals.update({'name': name})
|
||||
# create the audittrail log in super admin mode, only if a change has been detected
|
||||
if lines[(model_id, resource_id)]:
|
||||
log_id = pool.get('audittrail.log').create(cr, SUPERUSER_ID, vals)
|
||||
model = pool.get('ir.model').browse(cr, uid, model_id)
|
||||
create_log_line(cr, SUPERUSER_ID, log_id, model, lines[(model_id, resource_id)])
|
||||
return True
|
||||
|
||||
def check_rules(cr, uid, model, method):
|
||||
"""
|
||||
Checks if auditrails is installed for that db and then if one rule match
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID,
|
||||
@param model: value of _name of the object which values are being changed
|
||||
@param method: method to log: create, read, unlink,write,actions,workflow actions
|
||||
@return: True or False
|
||||
"""
|
||||
pool = openerp.registry(cr.dbname)
|
||||
if 'audittrail.rule' in pool.models:
|
||||
model_ids = pool.get('ir.model').search(cr, SUPERUSER_ID, [('model', '=', model)])
|
||||
model_id = model_ids and model_ids[0] or False
|
||||
if model_id:
|
||||
rule_ids = pool.get('audittrail.rule').search(cr, SUPERUSER_ID, [('object_id', '=', model_id), ('state', '=', 'subscribed')])
|
||||
for rule in pool.get('audittrail.rule').read(cr, SUPERUSER_ID, rule_ids, ['user_id','log_read','log_write','log_create','log_unlink','log_action','log_workflow']):
|
||||
if len(rule['user_id']) == 0 or uid in rule['user_id']:
|
||||
if rule.get('log_'+method,0):
|
||||
return True
|
||||
elif method not in ('default_get','read','fields_view_get','fields_get','search','search_count','name_search','name_get','get','request_get', 'get_sc', 'unlink', 'write', 'create', 'read_group', 'import_data'):
|
||||
if rule['log_action']:
|
||||
return True
|
||||
|
||||
# Replace the openerp.service.model functions.
|
||||
|
||||
original_execute_cr = openerp.service.model.execute_cr
|
||||
original_exec_workflow_cr = openerp.service.model.exec_workflow_cr
|
||||
|
||||
def execute_cr(cr, uid, model, method, *args, **kw):
|
||||
fct_src = original_execute_cr
|
||||
if check_rules(cr,uid,model,method):
|
||||
return log_fct(cr, uid, model, method, fct_src, *args, **kw)
|
||||
return fct_src(cr, uid, model, method, *args, **kw)
|
||||
|
||||
def exec_workflow_cr(cr, uid, model, method, *args, **kw):
|
||||
fct_src = original_exec_workflow_cr
|
||||
if check_rules(cr,uid,model,'workflow'):
|
||||
return log_fct(cr, uid, model, method, fct_src, *args, **kw)
|
||||
return fct_src(cr, uid, model, method, *args, **kw)
|
||||
|
||||
openerp.service.model.execute_cr = execute_cr
|
||||
openerp.service.model.exec_workflow_cr = exec_workflow_cr
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record model="audittrail.rule" id="demo_audittrail_rule">
|
||||
<field name="name">Audit on Partners</field>
|
||||
<field name="object_id" search="[('model','=','res.partner')]"/>
|
||||
<field name="user_id" search="[]"/>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,185 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<menuitem id="menu_audit" name="Audit" parent="base.menu_reporting" sequence="50" groups="base.group_system"/>
|
||||
|
||||
<!-- Audittrail Rule -->
|
||||
|
||||
<record model="ir.ui.view" id="view_audittrail_rule_form">
|
||||
<field name="name">audittrail.rule.form</field>
|
||||
<field name="model">audittrail.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="AuditTrail Rule" version="7.0">
|
||||
<header>
|
||||
<button string="_Subscribe" name="subscribe" icon="gtk-ok"
|
||||
type="object" states="draft"/>
|
||||
<button string="UnSubscribe" name="unsubscribe" icon="gtk-cancel"
|
||||
type="object" states="subscribed"/>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group col="4">
|
||||
<field name="name" required="1"/>
|
||||
<field name="object_id"/>
|
||||
<field name="log_read"/>
|
||||
<field name="log_write"/>
|
||||
<field name="log_unlink"/>
|
||||
<field name="log_create"/>
|
||||
<field name="log_action"/>
|
||||
<field name="log_workflow"/>
|
||||
<separator string="Users (if User is not added then it will applicable for all users)" colspan="4"/>
|
||||
<field name="user_id" colspan="4" nolabel="1"/>
|
||||
<field name="action_id" colspan="4" readonly="1" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_audittrail_rule_tree">
|
||||
<field name="name">audittrail.rule.tree</field>
|
||||
<field name="model">audittrail.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree colors="blue:state == 'draft';black:state == 'subscribed'" string="AuditTrail Rules">
|
||||
<field name="name"/>
|
||||
<field name="object_id"/>
|
||||
<field name="log_read"/>
|
||||
<field name="log_write"/>
|
||||
<field name="log_unlink"/>
|
||||
<field name="log_create"/>
|
||||
<field name="log_action"/>
|
||||
<field name="log_workflow"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_audittrail_rule_search" model="ir.ui.view">
|
||||
<field name="name">audittrail.rule.search</field>
|
||||
<field name="model">audittrail.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Audittrail Rule">
|
||||
<field name="name" string="Audittrail Rule"/>
|
||||
<filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Draft Rule"/>
|
||||
<filter icon="terp-camera_test" string="Subscribed" domain="[('state','=','subscribed')]" help="Subscribed Rule"/>
|
||||
<field name="object_id" string="Model"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_audittrail_rule_tree">
|
||||
<field name="name">Audit Rules</field>
|
||||
<field name="res_model">audittrail.rule</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_draft': 1}</field>
|
||||
<field name="search_view_id" ref="view_audittrail_rule_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_action_audittrail_rule_tree" parent="menu_audit" action="action_audittrail_rule_tree"/>
|
||||
|
||||
<!-- AuditTrail Log -->
|
||||
|
||||
<record model="ir.ui.view" id="view_audittrail_log_form">
|
||||
<field name="name">audittrail.log.form</field>
|
||||
<field name="model">audittrail.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="AuditTrail Logs" version="7.0">
|
||||
<sheet>
|
||||
<group col="4">
|
||||
<field name="timestamp" required="1" readonly="1"/>
|
||||
<field name="user_id" readonly="1"/>
|
||||
<field name="method" readonly="1"/>
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="res_id" readonly="1"/>
|
||||
<field name="object_id" readonly="1"/>
|
||||
</group>
|
||||
<field name="line_ids" mode="tree"
|
||||
widget="one2many_list" readonly="1">
|
||||
<form string="Log Lines" version="7.0">
|
||||
<group col="4">
|
||||
<field name="field_id" colspan="4"
|
||||
readonly="1"/>
|
||||
<newline/>
|
||||
<field name="field_description" colspan="4"
|
||||
readonly="1"/>
|
||||
<newline/>
|
||||
<separator string="Old Value : "
|
||||
colspan="2"/>
|
||||
<separator string="New Value : "
|
||||
colspan="2"/>
|
||||
<newline/>
|
||||
<field name="old_value" nolabel="1"
|
||||
colspan="2" readonly="1"/>
|
||||
<field name="new_value" nolabel="1"
|
||||
colspan="2" readonly="1"/>
|
||||
<newline/>
|
||||
<separator string="Old Value Text : "
|
||||
colspan="2"/>
|
||||
<separator string="New Value Text: "
|
||||
colspan="2"/>
|
||||
<newline/>
|
||||
<field name="old_value_text" nolabel="1"
|
||||
colspan="2" readonly="1"/>
|
||||
<field name="new_value_text" nolabel="1"
|
||||
colspan="2" readonly="1"/>
|
||||
</group>
|
||||
</form>
|
||||
<tree string="Log Lines">
|
||||
<field name="field_description"/>
|
||||
<field name="old_value_text"/>
|
||||
<field name="new_value_text"/>
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_audittrail_log_tree">
|
||||
<field name="name">audittrail.log.tree</field>
|
||||
<field name="model">audittrail.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="AuditTrail Logs" create="false">
|
||||
<field name="timestamp"/>
|
||||
<field name="name"/>
|
||||
<field name="object_id"/>
|
||||
<field name="method"/>
|
||||
<field name="user_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_audittrail_log_search" model="ir.ui.view">
|
||||
<field name="name">audittrail.log.search</field>
|
||||
<field name="model">audittrail.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Audittrail Log">
|
||||
<field name="name" string="Audittrail Log"/>
|
||||
<field name="object_id" string="Model"/>
|
||||
<field name="user_id"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="User" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
|
||||
<filter string="Object" icon="terp-stock_align_left_24" domain="[]" context="{'group_by':'object_id'}"/>
|
||||
<filter string="Audit Month" icon="terp-go-month" domain="[]" context="{'group_by':'timestamp'}" help="Audit Date by Month"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_audittrail_log_tree">
|
||||
<field name="name">Audit Logs</field>
|
||||
<field name="res_model">audittrail.log</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="search_view_id" ref="view_audittrail_log_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_audit_logs" name="Audit Logs" parent="menu_audit" action="action_audittrail_log_tree"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,5 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_audittrail_rule_all_users,audittrail rule all,model_audittrail_rule,base.group_system,1,1,1,0
|
||||
access_audittrail_rule_all_access,audittrail rule all,model_audittrail_rule,base.group_erp_manager,1,1,1,0
|
||||
access_audittrail_log_all_users,audittrail log all,model_audittrail_log,base.group_user,1,0,1,0
|
||||
access_audittrail_log_line_all_users,audittrail log line all,model_audittrail_log_line,base.group_user,1,0,1,0
|
|
|
@ -1,65 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
import time
|
||||
|
||||
class audittrail_view_log(osv.osv_memory):
|
||||
|
||||
_name = "audittrail.view.log"
|
||||
_description = "View Log"
|
||||
_columns = {
|
||||
'from':fields.datetime('Log From'),
|
||||
'to':fields.datetime('Log To', required = True)
|
||||
}
|
||||
_defaults = {
|
||||
'to': lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
def log_open_window(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Open Log form from given date range..
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param ids: List of audittrail view log’s IDs.
|
||||
@return: Dictionary of audittrail log form on given date range.
|
||||
"""
|
||||
|
||||
mod_obj = self.pool.get('ir.model.data')
|
||||
act_obj = self.pool.get('ir.actions.act_window')
|
||||
result = mod_obj._get_id(cr, uid, 'audittrail', 'action_audittrail_log_tree')
|
||||
id = mod_obj.read(cr, uid, [result], ['res_id'], context=context)[0]['res_id']
|
||||
result = act_obj.read(cr, uid, [id], context=context)[0]
|
||||
|
||||
#start Loop
|
||||
for datas in self.read(cr, uid, ids, context=context):
|
||||
if not datas.get('from', None):
|
||||
if datas.get('to') <> time.strftime("%Y-%m-%d %H:%M:%S"):
|
||||
result['domain'] = str([('timestamp', '<', datas.get('to'))])
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
result['domain'] = str([('timestamp', '>', datas.get('from', None)), ('timestamp', '<', datas.get('to'))])
|
||||
#End Loop
|
||||
return result
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<!-- Audittrail View Log wizard-->
|
||||
|
||||
<record id="view_audittrail_view_log" model="ir.ui.view">
|
||||
<field name="name">audittrail.view.log.form</field>
|
||||
<field name="model">audittrail.view.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Audit Logs" version="7.0">
|
||||
<group col="4">
|
||||
<field name="from"/>
|
||||
<field name="to"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Open Logs" name="log_open_window" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action for audittrail view log wizard -->
|
||||
|
||||
<record id="action_audittrail_view_log" model="ir.actions.act_window">
|
||||
<field name="name">View log</field>
|
||||
<field name="res_model">audittrail.view.log</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_audittrail_view_log"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -38,5 +38,4 @@ Re-implement openerp's file import system:
|
|||
'static/src/js/import.js',
|
||||
],
|
||||
'qweb': ['static/src/xml/import.xml'],
|
||||
'test': ['static/test/states.js'],
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
$(document).ready(function () {
|
||||
module('foo');
|
||||
test('dummy', function () {
|
||||
ok(42);
|
||||
});
|
||||
});
|
|
@ -24,9 +24,6 @@ from openerp.addons.web.controllers.main import manifest_list, module_boot, html
|
|||
drivers = {}
|
||||
|
||||
class Proxy(http.Controller):
|
||||
def __init__(self):
|
||||
self.scale = 'closed'
|
||||
self.scale_weight = 0.0
|
||||
|
||||
def get_status(self):
|
||||
statuses = {}
|
||||
|
@ -154,40 +151,6 @@ class Proxy(http.Controller):
|
|||
"""
|
||||
print "help_canceled"
|
||||
|
||||
@http.route('/hw_proxy/weighting_start', type='json', auth='none', cors='*')
|
||||
def weighting_start(self):
|
||||
if self.scale == 'closed':
|
||||
print "Opening (Fake) Connection to Scale..."
|
||||
self.scale = 'open'
|
||||
self.scale_weight = 0.0
|
||||
time.sleep(0.1)
|
||||
print "... Scale Open."
|
||||
else:
|
||||
print "WARNING: Scale already Connected !!!"
|
||||
|
||||
@http.route('/hw_proxy/weighting_read_kg', type='json', auth='none', cors='*')
|
||||
def weighting_read_kg(self):
|
||||
if self.scale == 'open':
|
||||
print "Reading Scale..."
|
||||
time.sleep(0.025)
|
||||
self.scale_weight += 0.01
|
||||
print "... Done."
|
||||
return self.scale_weight
|
||||
else:
|
||||
print "WARNING: Reading closed scale !!!"
|
||||
return 0.0
|
||||
|
||||
@http.route('/hw_proxy/weighting_end', type='json', auth='none', cors='*')
|
||||
def weighting_end(self):
|
||||
if self.scale == 'open':
|
||||
print "Closing Connection to Scale ..."
|
||||
self.scale = 'closed'
|
||||
self.scale_weight = 0.0
|
||||
time.sleep(0.1)
|
||||
print "... Scale Closed."
|
||||
else:
|
||||
print "WARNING: Scale already Closed !!!"
|
||||
|
||||
@http.route('/hw_proxy/payment_request', type='json', auth='none', cors='*')
|
||||
def payment_request(self, price):
|
||||
"""
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import audittrail_view_log
|
||||
import controllers
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2010-Today OpenERP S.A. (<http://www.openerp.com>).
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
|
@ -21,22 +21,25 @@
|
|||
|
||||
|
||||
{
|
||||
'name': 'Portal Project Long Term',
|
||||
'name': 'Weighting Scale Hardware Driver',
|
||||
'version': '1.0',
|
||||
'category': 'Tools',
|
||||
'complexity': 'easy',
|
||||
'category': 'Hardware Drivers',
|
||||
'sequence': 6,
|
||||
'summary': 'Hardware Driver for Weighting Scales',
|
||||
'description': """
|
||||
This module adds necessary security rules and access rights for project long term and portal.
|
||||
=============================================================================================
|
||||
""",
|
||||
Barcode Scanner Hardware Driver
|
||||
================================
|
||||
|
||||
This module allows the point of sale to connect to a scale using a USB HSM Serial Scale Interface,
|
||||
such as the Mettler Toledo Ariva.
|
||||
|
||||
""",
|
||||
'author': 'OpenERP SA',
|
||||
'depends': ['project_long_term', 'portal'],
|
||||
'data': [
|
||||
'security/portal_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'depends': ['hw_proxy'],
|
||||
'test': [
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'category': 'Hidden',
|
||||
'auto_install': False,
|
||||
}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,3 @@
|
|||
import main
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from os import listdir
|
||||
from os.path import join
|
||||
from threading import Thread, Lock
|
||||
from select import select
|
||||
from Queue import Queue, Empty
|
||||
|
||||
import openerp
|
||||
import openerp.addons.hw_proxy.controllers.main as hw_proxy
|
||||
from openerp import http
|
||||
from openerp.http import request
|
||||
from openerp.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
_logger.error('OpenERP module hw_scale depends on the pyserial python module')
|
||||
serial = None
|
||||
|
||||
|
||||
class Scale(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.lock = Lock()
|
||||
self.scalelock = Lock()
|
||||
self.status = {'status':'connecting', 'messages':[]}
|
||||
self.input_dir = '/dev/serial/by-id/'
|
||||
self.weight = 0
|
||||
self.weight_info = 'ok'
|
||||
self.device = None
|
||||
|
||||
def lockedstart(self):
|
||||
with self.lock:
|
||||
if not self.isAlive():
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def set_status(self, status, message = None):
|
||||
if status == self.status['status']:
|
||||
if message != None and message != self.status['messages'][-1]:
|
||||
self.status['messages'].append(message)
|
||||
else:
|
||||
self.status['status'] = status
|
||||
if message:
|
||||
self.status['messages'] = [message]
|
||||
else:
|
||||
self.status['messages'] = []
|
||||
|
||||
if status == 'error' and message:
|
||||
_logger.error('Scale Error: '+message)
|
||||
elif status == 'disconnected' and message:
|
||||
_logger.warning('Disconnected Scale: '+message)
|
||||
|
||||
def get_device(self):
|
||||
try:
|
||||
devices = [ device for device in listdir(self.input_dir)]
|
||||
scales = [ device for device in devices if ('mettler' in device.lower()) or ('toledo' in device.lower()) ]
|
||||
if len(scales) > 0:
|
||||
print join(self.input_dir,scales[0])
|
||||
self.set_status('connected','Connected to '+scales[0])
|
||||
return serial.Serial(join(self.input_dir,scales[0]),
|
||||
baudrate = 9600,
|
||||
bytesize = serial.SEVENBITS,
|
||||
stopbits = serial.STOPBITS_ONE,
|
||||
parity = serial.PARITY_EVEN,
|
||||
#xonxoff = serial.XON,
|
||||
timeout = 0.01,
|
||||
writeTimeout= 0.01)
|
||||
else:
|
||||
self.set_status('disconnected','Scale Not Found')
|
||||
return None
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
return None
|
||||
|
||||
def get_weight(self):
|
||||
self.lockedstart()
|
||||
return self.weight
|
||||
|
||||
def get_weight_info(self):
|
||||
self.lockedstart()
|
||||
return self.weight_info
|
||||
|
||||
def get_status(self):
|
||||
self.lockedstart()
|
||||
return self.status
|
||||
|
||||
def read_weight(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('W')
|
||||
time.sleep(0.1)
|
||||
answer = []
|
||||
|
||||
while True:
|
||||
char = self.device.read(1)
|
||||
if not char:
|
||||
break
|
||||
else:
|
||||
answer.append(char)
|
||||
|
||||
if '?' in answer:
|
||||
stat = ord(answer[answer.index('?')+1])
|
||||
if stat == 0:
|
||||
self.weight_info = 'ok'
|
||||
else:
|
||||
self.weight_info = []
|
||||
if stat & 1 :
|
||||
self.weight_info.append('moving')
|
||||
if stat & 1 << 1:
|
||||
self.weight_info.append('over_capacity')
|
||||
if stat & 1 << 2:
|
||||
self.weight_info.append('negative')
|
||||
self.weight = 0.0
|
||||
if stat & 1 << 3:
|
||||
self.weight_info.append('outside_zero_capture_range')
|
||||
if stat & 1 << 4:
|
||||
self.weight_info.append('center_of_zero')
|
||||
if stat & 1 << 5:
|
||||
self.weight_info.append('net_weight')
|
||||
else:
|
||||
answer = answer[1:-1]
|
||||
if 'N' in answer:
|
||||
answer = answer[0:-1]
|
||||
try:
|
||||
self.weight = float(''.join(answer))
|
||||
except ValueError as v:
|
||||
self.set_status('error','No data Received, please power-cycle the scale');
|
||||
self.device = None
|
||||
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.device = None
|
||||
|
||||
def set_zero(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('Z')
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.device = None
|
||||
|
||||
def set_tare(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('T')
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.device = None
|
||||
|
||||
def clear_tare(self):
|
||||
with self.scalelock:
|
||||
if self.device:
|
||||
try:
|
||||
self.device.write('C')
|
||||
except Exception as e:
|
||||
self.set_status('error',str(e))
|
||||
self.device = None
|
||||
|
||||
def run(self):
|
||||
self.device = None
|
||||
|
||||
while True:
|
||||
if self.device:
|
||||
self.read_weight()
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
with self.scalelock:
|
||||
self.device = self.get_device()
|
||||
if not self.device:
|
||||
time.sleep(5)
|
||||
|
||||
s = Scale()
|
||||
|
||||
hw_proxy.drivers['scale'] = s
|
||||
|
||||
class ScaleDriver(hw_proxy.Proxy):
|
||||
@http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*')
|
||||
def scale_read(self):
|
||||
return {'weight':s.get_weight(), 'unit':'kg', 'info':s.get_weight_info()}
|
||||
|
||||
@http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*')
|
||||
def scale_zero(self):
|
||||
s.set_zero()
|
||||
return True
|
||||
|
||||
@http.route('/hw_proxy/scale_tare/', type='json', auth='none', cors='*')
|
||||
def scale_tare(self):
|
||||
s.set_tare()
|
||||
return True
|
||||
|
||||
@http.route('/hw_proxy/scale_clear_tare/', type='json', auth='none', cors='*')
|
||||
def scale_clear_tare(self):
|
||||
s.clear_tare()
|
||||
return True
|
||||
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
<!-- Short thread: Admin ask, Agrolait answer [DEMO: mark thread as done] -->
|
||||
<record id="msg_discus1" model="mail.message">
|
||||
<field name="subject">Feedback about our On Site Assistance</field>
|
||||
<field name="body"><![CDATA[<p>Hi Virginie,</p><p>I writing to you about our <i>On Site Assistance Service</i> that we delivered to Agrolait last week. Do you have any feedback or remark about our service? I noticed you requested new IP phones. Will it be used for new employees, or did you have any issue with the ones we provided?<br />Best regards,</p>]]></field>
|
||||
<field name="body"><![CDATA[<p>Hi Virginie,</p><p>I wrote to you about our <i>On Site Assistance Service</i> that we delivered to Agrolait last week. Do you have any feedback or remark about our service? I noticed you requested new IP phones. Will it be used for new employees, or did you have any issue with the ones we provided?<br />Best regards,</p>]]></field>
|
||||
<field name="type">comment</field>
|
||||
<field name="subtype_id" ref="mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_root"/>
|
||||
|
|
|
@ -49,17 +49,11 @@
|
|||
<field name="name">Members Analysis</field>
|
||||
<field name="res_model">report.membership</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="search_view_id" ref="view_report_membership_search"/>
|
||||
<field name="context">{"search_default_year":1,"search_default_member":1, 'search_default_Revenue':1, 'search_default_this_month':1, 'search_default_salesman':1,'group_by_no_leaf':1}</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="action_report_membership_tree_view2">
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="view_report_membership_graph1"/>
|
||||
<field name="act_window_id" ref="action_report_membership_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Members Analysis" parent="base.menu_report_association"
|
||||
action="action_report_membership_tree"
|
||||
id="menu_report_membership"
|
||||
|
|
|
@ -739,12 +739,10 @@
|
|||
</field>
|
||||
<group string="Features" >
|
||||
<group>
|
||||
<field name="iface_cashdrawer" />
|
||||
<field name="iface_vkeyboard" />
|
||||
<field name="iface_invoicing" />
|
||||
<field name="iface_electronic_scale" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="iface_vkeyboard" />
|
||||
<field name="iface_big_scrollbars" />
|
||||
</group>
|
||||
</group>
|
||||
|
@ -752,6 +750,8 @@
|
|||
<field name="proxy_ip" />
|
||||
<field name="iface_print_via_proxy" />
|
||||
<field name="iface_scan_via_proxy" />
|
||||
<field name="iface_electronic_scale" />
|
||||
<field name="iface_cashdrawer" />
|
||||
</group>
|
||||
<group string="Receipt" >
|
||||
<field name="receipt_header" placeholder="A custom receipt header message"/>
|
||||
|
|
|
@ -384,47 +384,17 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
|
|||
return this.message('help_canceled');
|
||||
},
|
||||
|
||||
//the client is starting to weight
|
||||
weighting_start: function(){
|
||||
var ret = new $.Deferred();
|
||||
if(!this.weighting){
|
||||
this.weighting = true;
|
||||
this.message('weighting_start').always(function(){
|
||||
ret.resolve();
|
||||
});
|
||||
}else{
|
||||
console.error('Weighting already started!!!');
|
||||
ret.resolve();
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
// the client has finished weighting products
|
||||
weighting_end: function(){
|
||||
var ret = new $.Deferred();
|
||||
if(this.weighting){
|
||||
this.weighting = false;
|
||||
this.message('weighting_end').always(function(){
|
||||
ret.resolve();
|
||||
});
|
||||
}else{
|
||||
console.error('Weighting already ended !!!');
|
||||
ret.resolve();
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
//returns the weight on the scale.
|
||||
// is called at regular interval (up to 10x/sec) between a weighting_start()
|
||||
// and a weighting_end()
|
||||
weighting_read_kg: function(){
|
||||
// returns the weight on the scale.
|
||||
scale_read: function(){
|
||||
var self = this;
|
||||
var ret = new $.Deferred();
|
||||
this.message('weighting_read_kg',{})
|
||||
console.log('scale_read');
|
||||
this.message('scale_read',{})
|
||||
.then(function(weight){
|
||||
console.log(weight)
|
||||
ret.resolve(self.use_debug_weight ? self.debug_weight : weight);
|
||||
}, function(){ //failed to read weight
|
||||
ret.resolve(self.use_debug_weight ? self.debug_weight : 0.0);
|
||||
ret.resolve(self.use_debug_weight ? self.debug_weight : {weight:0.0, unit:'Kg', info:'ok'});
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
|
|
|
@ -526,12 +526,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
|||
});
|
||||
|
||||
queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_start()
|
||||
},{ important: true });
|
||||
|
||||
queue.schedule(function(){
|
||||
return self.pos.proxy.weighting_read_kg().then(function(weight){
|
||||
self.set_weight(weight);
|
||||
return self.pos.proxy.scale_read().then(function(weight){
|
||||
self.set_weight(weight.weight);
|
||||
});
|
||||
},{duration:50, repeat: true});
|
||||
|
||||
|
@ -584,9 +580,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
|||
$('body').off('keyup',this.hotkey_handler);
|
||||
|
||||
this.pos.proxy_queue.clear();
|
||||
this.pos.proxy_queue.schedule(function(){
|
||||
self.pos.proxy.weighting_end();
|
||||
},{ important: true });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -693,7 +693,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
|||
'open_cashbox',
|
||||
'print_receipt',
|
||||
'print_pdf_invoice',
|
||||
'weighting_read_kg',
|
||||
'scale_read',
|
||||
'payment_status',
|
||||
],
|
||||
minimized: false,
|
||||
|
@ -811,12 +811,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
|||
self.pos.proxy.add_notification('transaction_end',function(){
|
||||
self.$('.status.transaction').removeClass('on');
|
||||
});
|
||||
self.pos.proxy.add_notification('weighting_start',function(){
|
||||
self.$('.status.weighting').addClass('on');
|
||||
});
|
||||
self.pos.proxy.add_notification('weighting_end',function(){
|
||||
self.$('.status.weighting').removeClass('on');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -876,6 +870,14 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
|||
msg += _t('Printer');
|
||||
}
|
||||
}
|
||||
if( this.pos.config.iface_electronic_scale ){
|
||||
var scale = status.drivers.scale ? status.drivers.scale.status : false;
|
||||
if( scale != 'connected' && scale != 'connecting' ){
|
||||
warning = true;
|
||||
msg = msg ? msg + ' & ' : msg;
|
||||
msg += _t('Scale');
|
||||
}
|
||||
}
|
||||
msg = msg ? msg + ' ' + _t('Offline') : msg;
|
||||
this.set_status(warning ? 'warning' : 'connected', msg);
|
||||
}else{
|
||||
|
|
|
@ -692,7 +692,7 @@
|
|||
<li class="event open_cashbox">Open Cashbox</li>
|
||||
<li class="event print_receipt">Print Receipt</li>
|
||||
<li class="event print_pdf_invoice">Print Invoice</li>
|
||||
<li class="event weighting_read_kg">Read Weighting Scale</li>
|
||||
<li class="event scale_read">Read Weighting Scale</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2010-Today OpenERP S.A. (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
|
@ -1,3 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_issues,project_phase,project_long_term.model_project_phase,base.group_portal,1,0,0,0
|
||||
access_issues_public,project_phase_public,project_long_term.model_project_phase,base.group_public,1,0,0,0
|
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="portal_project_long_term_rule" model="ir.rule">
|
||||
<field name="name">Project/Phase: portal users: public or (portal and colleagues following) or (followers and following)</field>
|
||||
<field name="model_id" ref="project_long_term.model_project_phase"/>
|
||||
<field name="domain_force">[('project_id.privacy_visibility', 'in', ['public', 'portal'])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_phase_public_rule">
|
||||
<field name="name">Project/Phase: public users: public only</field>
|
||||
<field name="model_id" ref="project_long_term.model_project_phase"/>
|
||||
<field name="domain_force">[('project_id.privacy_visibility', '=', 'public')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,24 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import project_gtd
|
||||
import wizard
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
{
|
||||
'name': 'Todo Lists',
|
||||
'version': '1.0',
|
||||
'category': 'Project Management',
|
||||
'sequence': 100,
|
||||
'summary': 'Personal Tasks, Contexts, Timeboxes',
|
||||
'description': """
|
||||
Implement concepts of the "Getting Things Done" methodology
|
||||
===========================================================
|
||||
|
||||
This module implements a simple personal to-do list based on tasks. It adds an editable list of tasks simplified to the minimum required fields in the project application.
|
||||
|
||||
The to-do list is based on the GTD methodology. This world-wide used methodology is used for personal time management improvement.
|
||||
|
||||
Getting Things Done (commonly abbreviated as GTD) is an action management method created by David Allen, and described in a book of the same name.
|
||||
|
||||
GTD rests on the principle that a person needs to move tasks out of the mind by recording them externally. That way, the mind is freed from the job of remembering everything that needs to be done, and can concentrate on actually performing those tasks.
|
||||
""",
|
||||
'author': 'OpenERP SA',
|
||||
'images': ['images/project_gtd.jpeg'],
|
||||
'depends': ['project'],
|
||||
'data': [
|
||||
'project_gtd_data.xml',
|
||||
'project_gtd_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/project_gtd_empty_view.xml',
|
||||
'wizard/project_gtd_fill_view.xml',
|
||||
],
|
||||
'demo': ['project_gtd_demo.xml'],
|
||||
'test':['test/task_timebox.yml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,122 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import sys
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp import tools
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class project_gtd_context(osv.osv):
|
||||
_name = "project.gtd.context"
|
||||
_description = "Context"
|
||||
_columns = {
|
||||
'name': fields.char('Context', size=64, required=True, select=1, translate=1),
|
||||
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of contexts."),
|
||||
}
|
||||
_defaults = {
|
||||
'sequence': 1
|
||||
}
|
||||
_order = "sequence, name"
|
||||
|
||||
|
||||
|
||||
class project_gtd_timebox(osv.osv):
|
||||
_name = "project.gtd.timebox"
|
||||
_order = "sequence"
|
||||
_columns = {
|
||||
'name': fields.char('Timebox', size=64, required=True, select=1, translate=1),
|
||||
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of timebox."),
|
||||
'icon': fields.selection(tools.icons, 'Icon', size=64),
|
||||
}
|
||||
|
||||
|
||||
class project_task(osv.osv):
|
||||
_inherit = "project.task"
|
||||
_columns = {
|
||||
'timebox_id': fields.many2one('project.gtd.timebox', "Timebox",help="Time-laps during which task has to be treated"),
|
||||
'context_id': fields.many2one('project.gtd.context', "Context",help="The context place where user has to treat task"),
|
||||
}
|
||||
|
||||
def copy_data(self, cr, uid, id, default=None, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if not default:
|
||||
default = {}
|
||||
default['timebox_id'] = False
|
||||
default['context_id'] = False
|
||||
return super(project_task,self).copy_data(cr, uid, id, default, context)
|
||||
|
||||
def _get_context(self, cr, uid, context=None):
|
||||
ids = self.pool.get('project.gtd.context').search(cr, uid, [], context=context)
|
||||
return ids and ids[0] or False
|
||||
|
||||
_defaults = {
|
||||
'context_id': _get_context
|
||||
}
|
||||
def next_timebox(self, cr, uid, ids, *args):
|
||||
timebox_obj = self.pool.get('project.gtd.timebox')
|
||||
timebox_ids = timebox_obj.search(cr,uid,[])
|
||||
if not timebox_ids: return True
|
||||
for task in self.browse(cr,uid,ids):
|
||||
timebox = task.timebox_id.id
|
||||
if not timebox:
|
||||
self.write(cr, uid, task.id, {'timebox_id': timebox_ids[0]})
|
||||
elif timebox_ids.index(timebox) != len(timebox_ids)-1:
|
||||
index = timebox_ids.index(timebox)
|
||||
self.write(cr, uid, task.id, {'timebox_id': timebox_ids[index+1]})
|
||||
return True
|
||||
|
||||
def prev_timebox(self, cr, uid, ids, *args):
|
||||
timebox_obj = self.pool.get('project.gtd.timebox')
|
||||
timebox_ids = timebox_obj.search(cr,uid,[])
|
||||
for task in self.browse(cr,uid,ids):
|
||||
timebox = task.timebox_id.id
|
||||
if timebox:
|
||||
if timebox_ids.index(timebox):
|
||||
index = timebox_ids.index(timebox)
|
||||
self.write(cr, uid, task.id, {'timebox_id': timebox_ids[index - 1]})
|
||||
else:
|
||||
self.write(cr, uid, task.id, {'timebox_id': False})
|
||||
return True
|
||||
|
||||
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
|
||||
if not context: context = {}
|
||||
res = super(project_task,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
|
||||
search_extended = False
|
||||
timebox_obj = self.pool.get('project.gtd.timebox')
|
||||
if (res['type'] == 'search') and context.get('gtd', False):
|
||||
tt = timebox_obj.browse(cr, uid, timebox_obj.search(cr,uid,[]), context=context)
|
||||
search_extended =''
|
||||
for time in tt:
|
||||
if time.icon:
|
||||
icon = time.icon
|
||||
else :
|
||||
icon=""
|
||||
search_extended += '''<filter domain="[('timebox_id','=', ''' + str(time.id) + ''')]" icon="''' + icon + '''" string="''' + time.name + '''" context="{'user_invisible': True}"/>\n'''
|
||||
search_extended +='''<separator orientation="vertical"/>'''
|
||||
|
||||
res['arch'] = tools.ustr(res['arch']).replace('<separator name="gtdsep"/>', search_extended)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,40 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="project.gtd.context" id="context_office">
|
||||
<field name="name">Office</field>
|
||||
<field name="sequence">0</field>
|
||||
</record>
|
||||
<record model="project.gtd.context" id="context_travel">
|
||||
<field name="name">Travel</field>
|
||||
<field name="sequence">2</field>
|
||||
</record>
|
||||
|
||||
<record model="project.gtd.timebox" id="timebox_daily">
|
||||
<field name="name">Today</field>
|
||||
<field name="icon">terp-go-today</field>
|
||||
</record>
|
||||
<record model="project.gtd.timebox" id="timebox_weekly">
|
||||
<field name="name">This Week</field>
|
||||
<field name="icon">terp-go-week</field>
|
||||
</record>
|
||||
<record model="project.gtd.timebox" id="timebox_lt">
|
||||
<field name="name">Long Term</field>
|
||||
<field name="icon">terp-project</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- notify all employees of module installation -->
|
||||
<record model="mail.message" id="module_install_notification">
|
||||
<field name="model">mail.group</field>
|
||||
<field name="res_id" ref="mail.group_all_employees"/>
|
||||
<field name="type">notification</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="subject">Todo Lists application installed!</field>
|
||||
<field name="body"><![CDATA[<p>Add todo items on project tasks, to help you organize your work.</p><p>
|
||||
This application supports the Getting Things Done (GTD) methodology, based on David Allen's book.</p>]]></field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,35 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record model="project.gtd.context" id="context_home">
|
||||
<field name="name">Home</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
<record model="project.gtd.context" id="context_car">
|
||||
<field name="name">Car</field>
|
||||
<field name="sequence">1</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_10" model="project.task">
|
||||
<field name="timebox_id" ref="timebox_daily"/>
|
||||
<field name="context_id" ref="context_office"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_11" model="project.task">
|
||||
<field name="timebox_id" ref="timebox_daily"/>
|
||||
<field name="context_id" ref="context_office"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_12" model="project.task">
|
||||
<field name="timebox_id" ref="timebox_daily"/>
|
||||
<field name="context_id" ref="context_car"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_13" model="project.task">
|
||||
<field name="timebox_id" ref="timebox_daily"/>
|
||||
<field name="context_id" ref="context_car"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,132 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_gtd_context_tree">
|
||||
<field name="name">project.gtd.context.tree</field>
|
||||
<field name="model">project.gtd.context</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Context">
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_gtd_context_form">
|
||||
<field name="name">project.gtd.context.form</field>
|
||||
<field name="model">project.gtd.context</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Context" version="7.0">
|
||||
<group col="4">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="open_gtd_context_tree">
|
||||
<field name="name">Contexts</field>
|
||||
<field name="res_model">project.gtd.context</field>
|
||||
<field name="help">Contexts are defined in the "Getting Things Done" methodology. It allows you to categorize your tasks according to the context in which they have to be done: at the office, at home, when I take my car, etc.</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Contexts" id="menu_open_gtd_time_contexts"
|
||||
parent="project.menu_tasks_config" action="open_gtd_context_tree" groups="base.group_no_one"/>
|
||||
|
||||
<record model="ir.ui.view" id="view_gtd_timebox_tree">
|
||||
<field name="name">project.gtd.timebox.tree</field>
|
||||
<field name="model">project.gtd.timebox</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Timebox">
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="icon"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_gtd_timebox_form">
|
||||
<field name="name">project.gtd.timebox.form</field>
|
||||
<field name="model">project.gtd.timebox</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Timeboxes" version="7.0">
|
||||
<group col="4" string="Timebox Definition">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="icon"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="open_gtd_timebox_tree">
|
||||
<field name="name">Timeboxes</field>
|
||||
<field name="res_model">project.gtd.timebox</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_gtd_timebox_tree"/>
|
||||
<field name="help">Timeboxes are defined in the "Getting Things Done" methodology. A timebox defines a period of time in order to categorize your tasks: today, this week, this month, long term.</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Timeboxes" id="menu_open_gtd_time_timeboxes" parent="project.menu_tasks_config" action="open_gtd_timebox_tree" groups="base.group_no_one"/>
|
||||
|
||||
<record model="ir.ui.view" id="project_task_tree">
|
||||
<field name="name">project.task.tree.timebox</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_tree2" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="remaining_hours" position="after">
|
||||
<field string="Timeframe" name="timebox_id" invisible=" not context.get('gtd', False)"/>
|
||||
<field name="context_id" invisible="not context.get('context_show', False)" widget="selection"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="project_task">
|
||||
<field name="name">project.task.form.timebox</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="progress" position="after">
|
||||
<field name="context_id" widget="selection" options='{"no_open": True}'/>
|
||||
<field name="timebox_id" widget="selection" options='{"no_open": True}' string="Timeframe"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_task_gtd_search" model="ir.ui.view">
|
||||
<field name="name">project.task.gtd.search</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority">50</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="My Tasks">
|
||||
<field name="name" string="My Tasks"/>
|
||||
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter string="No Timebox" domain="[('timebox_id', '=', False)]" help="Tasks having no timebox assigned yet"/>
|
||||
<group expand="0" string="Display">
|
||||
<filter string="Context" name="context_show" context="{'context_show': True}" domain="[]" icon="terp-camera_test" help="Show the context field"/>
|
||||
<filter string="Deadlines" context="{'deadline_visible': False}" domain="[]" help="Show only tasks having a deadline" icon="terp-gnome-cpu-frequency-applet+"/>
|
||||
</group>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Stage" name="group_stage_id" context="{'group_by':'stage_id'}"/>
|
||||
<filter string="Timebox" name="group_timebox_id" context="{'group_by':'timebox_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="open_gtd_task">
|
||||
<field name="name">My Tasks</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="search_view_id" ref="view_task_gtd_search"/>
|
||||
<field name="context">{'set_editable':True,'set_visible':True,'gtd':True,'user_invisible':True, "search_default_open": 1}</field>
|
||||
<field name="domain">[('user_id','=',uid)]</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban,tree,form,calendar,gantt,graph</field>
|
||||
</record>
|
||||
<menuitem action="open_gtd_task" id="menu_open_gtd_timebox_tree" parent="project.menu_project_management" sequence="10"/>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,5 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_project_gtd_context_user,project.gtd.context project user,model_project_gtd_context,project.group_project_user,1,0,0,0
|
||||
access_project_gtd_timebox_user,project.gtd.timebox project user,model_project_gtd_timebox,project.group_project_user,1,0,0,0
|
||||
access_project_gtd_context_manager,project.gtd.context project manager,model_project_gtd_context,project.group_project_manager,1,1,1,1
|
||||
access_project_gtd_timebox_manager,project.gtd.timebox project manager,model_project_gtd_timebox,project.group_project_manager,1,1,1,1
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.4 KiB |
|
@ -1,45 +0,0 @@
|
|||
-
|
||||
In order to test the process of Timebox in Project Management module,
|
||||
I set my task from Daily to Weekly Timebox through Plannify Timebox
|
||||
-
|
||||
!record {model: project.timebox.fill.plan, id: plan_id}:
|
||||
task_ids: [project.project_task_10]
|
||||
timebox_id: timebox_daily
|
||||
timebox_to_id: timebox_weekly
|
||||
-
|
||||
I set the task from Daily Timebox to Weekly Timebox
|
||||
-
|
||||
!python {model: project.timebox.fill.plan}: |
|
||||
self.process(cr, uid, [ref("plan_id")])
|
||||
-
|
||||
I check task is set to Weekly Timebox
|
||||
-
|
||||
!assert {model: project.task, id: project.project_task_10, string: Task should be set to weekly timebox}:
|
||||
- timebox_id.id == ref("timebox_weekly")
|
||||
-
|
||||
I Empty the Weekly Timebox
|
||||
-
|
||||
!python {model: project.timebox.empty}: |
|
||||
self._empty(cr, uid, {"active_model": "project.gtd.timebox",
|
||||
"active_ids":[ref("timebox_weekly")],
|
||||
"active_id": ref("timebox_weekly"),
|
||||
})
|
||||
-
|
||||
I check task 'Develop Module in Sale Management' is no more in Weekly Timebox
|
||||
-
|
||||
!assert {model: project.task, id: project.project_task_10 , string: Task is not in Weekly Timebox }:
|
||||
- timebox_id.id != ref("timebox_weekly")
|
||||
-
|
||||
I set Previous Timebox on task
|
||||
-
|
||||
!python {model: project.task}: |
|
||||
previous_timebox = self.prev_timebox(cr, uid, [ref("project.project_task_10")],
|
||||
{'active_ids': [ref("project_gtd.menu_open_gtd_timebox_tree")],})
|
||||
assert previous_timebox == True, "I set Previous Timebox on task"
|
||||
-
|
||||
I set Next Timebox on task
|
||||
-
|
||||
!python {model: project.task}: |
|
||||
next_timebox = self.next_timebox(cr, uid, [ref("project.project_task_10")],
|
||||
{'active_ids': [ref("project_gtd.menu_open_gtd_timebox_tree")],})
|
||||
assert next_timebox == True, "I set Next Timebox on task"
|
|
@ -1,26 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import project_gtd_empty
|
||||
import project_gtd_fill
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class project_timebox_empty(osv.osv_memory):
|
||||
|
||||
_name = 'project.timebox.empty'
|
||||
_description = 'Project Timebox Empty'
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=32)
|
||||
}
|
||||
|
||||
def view_init(self, cr, uid, fields_list, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
self._empty(cr, uid, context=context)
|
||||
pass
|
||||
|
||||
def _empty(self, cr, uid, context=None):
|
||||
close = []
|
||||
up = []
|
||||
obj_tb = self.pool.get('project.gtd.timebox')
|
||||
obj_task = self.pool.get('project.task')
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
if not 'active_id' in context:
|
||||
return {}
|
||||
|
||||
ids = obj_tb.search(cr, uid, [], context=context)
|
||||
if not len(ids):
|
||||
raise osv.except_osv(_('Error!'), _('No timebox child of this one!'))
|
||||
tids = obj_task.search(cr, uid, [('timebox_id', '=', context['active_id'])])
|
||||
for task in obj_task.browse(cr, uid, tids, context):
|
||||
if (task.stage_id and task.stage_id.fold) or (task.user_id.id <> uid):
|
||||
close.append(task.id)
|
||||
else:
|
||||
up.append(task.id)
|
||||
if up:
|
||||
obj_task.write(cr, uid, up, {'timebox_id':ids[0]})
|
||||
if close:
|
||||
obj_task.write(cr, uid, close, {'timebox_id':False})
|
||||
return {}
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_project_gtd_empty" model="ir.ui.view">
|
||||
<field name="name">Empty Timebox</field>
|
||||
<field name="model">project.timebox.empty</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Empty Timebox" version="7.0">
|
||||
<label string="Timebox Empty Process Completed Successfully." />
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_gtd_empty" model="ir.actions.act_window">
|
||||
<field name="name">Empty Timebox</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.timebox.empty</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_project_gtd_empty"/>
|
||||
<field name="context">{'record_id' : active_id}</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.values" id="project_gtd_empty_values">
|
||||
<field name="model_id" ref="model_project_gtd_timebox" />
|
||||
<field name="name">Empty Timebox</field>
|
||||
<field name="key2">client_action_multi</field>
|
||||
<field name="value" eval="'ir.actions.act_window,' + str(ref('action_project_gtd_empty'))" />
|
||||
<field name="key">action</field>
|
||||
<field name="model">project.gtd.timebox</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,60 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
|
||||
class project_timebox_fill(osv.osv_memory):
|
||||
|
||||
_name = 'project.timebox.fill.plan'
|
||||
_description = 'Project Timebox Fill'
|
||||
_columns = {
|
||||
'timebox_id': fields.many2one('project.gtd.timebox', 'Get from Timebox', required=True),
|
||||
'timebox_to_id': fields.many2one('project.gtd.timebox', 'Set to Timebox', required=True),
|
||||
'task_ids': fields.many2many('project.task', 'project_task_rel', 'task_id', 'fill_id', 'Tasks selection')
|
||||
}
|
||||
|
||||
def _get_from_tb(self, cr, uid, context=None):
|
||||
ids = self.pool.get('project.gtd.timebox').search(cr, uid, [], context=context)
|
||||
return ids and ids[0] or False
|
||||
|
||||
def _get_to_tb(self, cr, uid, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if 'active_id' in context:
|
||||
return context['active_id']
|
||||
return False
|
||||
|
||||
_defaults = {
|
||||
'timebox_id': _get_from_tb,
|
||||
'timebox_to_id': _get_to_tb,
|
||||
}
|
||||
|
||||
def process(self, cr, uid, ids, context=None):
|
||||
if not ids:
|
||||
return {}
|
||||
data = self.read(cr, uid, ids, [], context=context)
|
||||
if not data[0]['task_ids']:
|
||||
return {}
|
||||
self.pool.get('project.task').write(cr, uid, data[0]['task_ids'], {'timebox_id':data[0]['timebox_to_id'][0]})
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_project_gtd_fill" model="ir.ui.view">
|
||||
<field name="name">Plannify Timebox</field>
|
||||
<field name="model">project.timebox.fill.plan</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Plannify Timebox" version="7.0">
|
||||
<group col="4">
|
||||
<field name="timebox_id" widget="selection" options='{"no_open": True}'/>
|
||||
<field name="timebox_to_id" widget="selection"/>
|
||||
</group>
|
||||
<field name="task_ids" domain="[('timebox_id','=',timebox_id),('state','=','open')]" />
|
||||
<footer>
|
||||
<button name="process" string="Add to Timebox" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_gtd_fill" model="ir.actions.act_window">
|
||||
<field name="name">Plannify Timebox</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.timebox.fill.plan</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_project_gtd_fill"/>
|
||||
<field name="context">{'record_id' : active_id}</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.values" id="project_gtd_fill_values">
|
||||
<field name="model_id" ref="model_project_gtd_timebox" />
|
||||
<field name="name">Plannify Timebox</field>
|
||||
<field name="key2">client_action_multi</field>
|
||||
<field name="value" eval="'ir.actions.act_window,' + str(ref('action_project_gtd_fill'))" />
|
||||
<field name="key">action</field>
|
||||
<field name="model">project.gtd.timebox</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import project_long_term
|
||||
import wizard
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,63 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'Long Term Projects',
|
||||
'version': '1.1',
|
||||
'author': 'OpenERP SA',
|
||||
'website': 'http://www.openerp.com',
|
||||
'category': 'Project Management',
|
||||
'images': ['images/project_phase_form.jpeg','images/project_phases.jpeg', 'images/resources_allocation.jpeg'],
|
||||
'depends': ['project'],
|
||||
'description': """
|
||||
Long Term Project management module that tracks planning, scheduling, resources allocation.
|
||||
===========================================================================================
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Manage Big project
|
||||
* Define various Phases of Project
|
||||
* Compute Phase Scheduling: Compute start date and end date of the phases
|
||||
which are in draft, open and pending state of the project given. If no
|
||||
project given then all the draft, open and pending state phases will be taken.
|
||||
* Compute Task Scheduling: This works same as the scheduler button on
|
||||
project.phase. It takes the project as argument and computes all the open,
|
||||
draft and pending tasks.
|
||||
* Schedule Tasks: All the tasks which are in draft, pending and open state
|
||||
are scheduled with taking the phase's start date.
|
||||
""",
|
||||
'demo': ['project_long_term_demo.xml'],
|
||||
'test': [
|
||||
'test/phase_process.yml',
|
||||
'test/task_process.yml',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'project_long_term_view.xml',
|
||||
'project_long_term_workflow.xml',
|
||||
'wizard/project_compute_phases_view.xml',
|
||||
'wizard/project_compute_tasks_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,296 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from datetime import datetime
|
||||
from openerp.tools.translate import _
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.addons.resource.faces import task as Task
|
||||
|
||||
class project_phase(osv.osv):
|
||||
_name = "project.phase"
|
||||
_description = "Project Phase"
|
||||
|
||||
def _check_recursion(self, cr, uid, ids, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
data_phase = self.browse(cr, uid, ids[0], context=context)
|
||||
prev_ids = data_phase.previous_phase_ids
|
||||
next_ids = data_phase.next_phase_ids
|
||||
# it should neither be in prev_ids nor in next_ids
|
||||
if (data_phase in prev_ids) or (data_phase in next_ids):
|
||||
return False
|
||||
ids = [id for id in prev_ids if id in next_ids]
|
||||
# both prev_ids and next_ids must be unique
|
||||
if ids:
|
||||
return False
|
||||
# unrelated project
|
||||
prev_ids = [rec.id for rec in prev_ids]
|
||||
next_ids = [rec.id for rec in next_ids]
|
||||
# iter prev_ids
|
||||
while prev_ids:
|
||||
cr.execute('SELECT distinct prv_phase_id FROM project_phase_rel WHERE next_phase_id IN %s', (tuple(prev_ids),))
|
||||
prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
|
||||
if data_phase.id in prv_phase_ids:
|
||||
return False
|
||||
ids = [id for id in prv_phase_ids if id in next_ids]
|
||||
if ids:
|
||||
return False
|
||||
prev_ids = prv_phase_ids
|
||||
# iter next_ids
|
||||
while next_ids:
|
||||
cr.execute('SELECT distinct next_phase_id FROM project_phase_rel WHERE prv_phase_id IN %s', (tuple(next_ids),))
|
||||
next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
|
||||
if data_phase.id in next_phase_ids:
|
||||
return False
|
||||
ids = [id for id in next_phase_ids if id in prev_ids]
|
||||
if ids:
|
||||
return False
|
||||
next_ids = next_phase_ids
|
||||
return True
|
||||
|
||||
def _check_dates(self, cr, uid, ids, context=None):
|
||||
for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
|
||||
if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _compute_progress(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
if not ids:
|
||||
return res
|
||||
for phase in self.browse(cr, uid, ids, context=context):
|
||||
if phase.state=='done':
|
||||
res[phase.id] = 100.0
|
||||
continue
|
||||
elif phase.state=="cancelled":
|
||||
res[phase.id] = 0.0
|
||||
continue
|
||||
elif not phase.task_ids:
|
||||
res[phase.id] = 0.0
|
||||
continue
|
||||
|
||||
tot = done = 0.0
|
||||
for task in phase.task_ids:
|
||||
tot += task.total_hours
|
||||
done += min(task.effective_hours, task.total_hours)
|
||||
|
||||
if not tot:
|
||||
res[phase.id] = 0.0
|
||||
else:
|
||||
res[phase.id] = round(100.0 * done / tot, 2)
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'name': fields.char("Name", size=64, required=True),
|
||||
'date_start': fields.datetime('Start Date', select=True, help="It's computed by the scheduler according the project date or the end date of the previous phase.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'date_end': fields.datetime('End Date', help=" It's computed by the scheduler according to the start date and the duration.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'constraint_date_start': fields.datetime('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'constraint_date_end': fields.datetime('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'project_id': fields.many2one('project.project', 'Project', required=True, select=True),
|
||||
'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
|
||||
'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
|
||||
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of phases."),
|
||||
'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'product_uom': fields.many2one('product.uom', 'Duration Unit of Measure', required=True, help="Unit of Measure (Unit of Measure) is the unit of measurement for Duration", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
|
||||
'user_force_ids': fields.many2many('res.users', string='Force Assigned Users'),
|
||||
'user_ids': fields.one2many('project.user.allocation', 'phase_id', "Assigned Users",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]},
|
||||
help="The resources on the project can be computed automatically by the scheduler."),
|
||||
'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'), ('pending', 'Pending'), ('done', 'Done')], 'Status', readonly=True, required=True,
|
||||
help='If the phase is created the status \'Draft\'.\n If the phase is started, the status becomes \'In Progress\'.\n If review is needed the phase is in \'Pending\' status.\
|
||||
\n If the phase is over, the status is set to \'Done\'.'),
|
||||
'progress': fields.function(_compute_progress, string='Progress', help="Computed based on related tasks"),
|
||||
}
|
||||
_defaults = {
|
||||
'state': 'draft',
|
||||
'sequence': 10,
|
||||
}
|
||||
_order = "project_id, date_start, sequence"
|
||||
_constraints = [
|
||||
(_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
|
||||
(_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
|
||||
]
|
||||
|
||||
def onchange_project(self, cr, uid, ids, project, context=None):
|
||||
return {}
|
||||
|
||||
def copy(self, cr, uid, id, default=None, context=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
if not default.get('name', False):
|
||||
default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
|
||||
return super(project_phase, self).copy(cr, uid, id, default, context)
|
||||
|
||||
def set_draft(self, cr, uid, ids, *args):
|
||||
self.write(cr, uid, ids, {'state': 'draft'})
|
||||
return True
|
||||
|
||||
def set_open(self, cr, uid, ids, *args):
|
||||
self.write(cr, uid, ids, {'state': 'open'})
|
||||
return True
|
||||
|
||||
def set_pending(self, cr, uid, ids, *args):
|
||||
self.write(cr, uid, ids, {'state': 'pending'})
|
||||
return True
|
||||
|
||||
def set_cancel(self, cr, uid, ids, *args):
|
||||
self.write(cr, uid, ids, {'state': 'cancelled'})
|
||||
return True
|
||||
|
||||
def set_done(self, cr, uid, ids, *args):
|
||||
self.write(cr, uid, ids, {'state': 'done'})
|
||||
return True
|
||||
|
||||
def generate_phase(self, cr, uid, phases, context=None):
|
||||
context = context or {}
|
||||
result = ""
|
||||
|
||||
task_pool = self.pool.get('project.task')
|
||||
for phase in phases:
|
||||
if phase.state in ('done','cancelled'):
|
||||
continue
|
||||
# FIXME: brittle and not working if context['lang'] != 'en_US'
|
||||
duration_uom = {
|
||||
'day(s)': 'd', 'days': 'd', 'day': 'd', 'd':'d',
|
||||
'month(s)': 'm', 'months': 'm', 'month':'month', 'm':'m',
|
||||
'week(s)': 'w', 'weeks': 'w', 'week': 'w', 'w':'w',
|
||||
'hour(s)': 'H', 'hours': 'H', 'hour': 'H', 'h':'H',
|
||||
}.get(phase.product_uom.name.lower(), "H")
|
||||
duration = str(phase.duration) + duration_uom
|
||||
result += '''
|
||||
def Phase_%s():
|
||||
effort = \"%s\"''' % (phase.id, duration)
|
||||
start = []
|
||||
if phase.constraint_date_start:
|
||||
start.append('datetime.datetime.strptime("'+str(phase.constraint_date_start)+'", "%Y-%m-%d %H:%M:%S")')
|
||||
for previous_phase in phase.previous_phase_ids:
|
||||
start.append("up.Phase_%s.end" % (previous_phase.id,))
|
||||
if start:
|
||||
result += '''
|
||||
start = max(%s)
|
||||
''' % (','.join(start))
|
||||
|
||||
if phase.user_force_ids:
|
||||
result += '''
|
||||
resource = %s
|
||||
''' % '|'.join(map(lambda x: 'User_'+str(x.id), phase.user_force_ids))
|
||||
|
||||
result += task_pool._generate_task(cr, uid, phase.task_ids, ident=8, context=context)
|
||||
result += "\n"
|
||||
|
||||
return result
|
||||
|
||||
class project_user_allocation(osv.osv):
|
||||
_name = 'project.user.allocation'
|
||||
_description = 'Phase User Allocation'
|
||||
_rec_name = 'user_id'
|
||||
_columns = {
|
||||
'user_id': fields.many2one('res.users', 'User', required=True),
|
||||
'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
|
||||
'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
|
||||
'date_start': fields.datetime('Start Date', help="Starting Date"),
|
||||
'date_end': fields.datetime('End Date', help="Ending Date"),
|
||||
}
|
||||
|
||||
class project(osv.osv):
|
||||
_inherit = "project.project"
|
||||
|
||||
def _phase_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = dict.fromkeys(ids, 0)
|
||||
phase_ids = self.pool.get('project.phase').search(cr, uid, [('project_id', 'in', ids)])
|
||||
for phase in self.pool.get('project.phase').browse(cr, uid, phase_ids, context):
|
||||
res[phase.project_id.id] += 1
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
|
||||
'phase_count': fields.function(_phase_count, type='integer', string="Open Phases"),
|
||||
}
|
||||
|
||||
def schedule_phases(self, cr, uid, ids, context=None):
|
||||
context = context or {}
|
||||
if type(ids) in (long, int,):
|
||||
ids = [ids]
|
||||
projects = self.browse(cr, uid, ids, context=context)
|
||||
result = self._schedule_header(cr, uid, ids, context=context)
|
||||
for project in projects:
|
||||
result += self._schedule_project(cr, uid, project, context=context)
|
||||
result += self.pool.get('project.phase').generate_phase(cr, uid, project.phase_ids, context=context)
|
||||
|
||||
local_dict = {}
|
||||
exec result in local_dict
|
||||
projects_gantt = Task.BalancedProject(local_dict['Project'])
|
||||
|
||||
for project in projects:
|
||||
project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
|
||||
for phase in project.phase_ids:
|
||||
if phase.state in ('done','cancelled'):
|
||||
continue
|
||||
# Maybe it's better to update than unlink/create if it already exists ?
|
||||
p = getattr(project_gantt, 'Phase_%d' % (phase.id,))
|
||||
|
||||
self.pool.get('project.user.allocation').unlink(cr, uid,
|
||||
[x.id for x in phase.user_ids],
|
||||
context=context
|
||||
)
|
||||
|
||||
for r in p.booked_resource:
|
||||
self.pool.get('project.user.allocation').create(cr, uid, {
|
||||
'user_id': int(r.name[5:]),
|
||||
'phase_id': phase.id,
|
||||
'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}, context=context)
|
||||
self.pool.get('project.phase').write(cr, uid, [phase.id], {
|
||||
'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}, context=context)
|
||||
return True
|
||||
|
||||
class account_analytic_account(osv.osv):
|
||||
_inherit = 'account.analytic.account'
|
||||
_description = 'Analytic Account'
|
||||
_columns = {
|
||||
'use_phases': fields.boolean('Phases', help="Check this field if you plan to use phase-based scheduling"),
|
||||
}
|
||||
|
||||
def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
|
||||
res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
|
||||
if template_id and 'value' in res:
|
||||
template = self.browse(cr, uid, template_id, context=context)
|
||||
res['value']['use_phases'] = template.use_phases
|
||||
return res
|
||||
|
||||
|
||||
def _trigger_project_creation(self, cr, uid, vals, context=None):
|
||||
if context is None: context = {}
|
||||
res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
|
||||
return res or (vals.get('use_phases') and not 'project_creation_in_progress' in context)
|
||||
|
||||
|
||||
class project_task(osv.osv):
|
||||
_inherit = "project.task"
|
||||
_columns = {
|
||||
'phase_id': fields.many2one('project.phase', 'Project Phase', domain="[('project_id', '=', project_id)]"),
|
||||
}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,111 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<!--
|
||||
This Demo data file Human Resources, Phases and Resources,Tasks allocation and also run scheduling of phase and tasks.
|
||||
-->
|
||||
|
||||
<record id="project.project_project_1" model="project.project">
|
||||
<field name="resource_calendar_id" ref="resource.timesheet_group1"/>
|
||||
<field name="use_phases" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Project Phases -->
|
||||
|
||||
<record id="project_phase_1" model="project.phase">
|
||||
<field eval="[(6, 0, [])]" name="previous_phase_ids"/>
|
||||
<field name="name">Collect Requirement and Make SRS</field>
|
||||
<field name="product_uom" ref="product.product_uom_day"/>
|
||||
<field eval="1" name="sequence"/>
|
||||
<field name="duration">30</field>
|
||||
<field name="constraint_date_start" eval="time.strftime('%Y-%m-01 10:00:00')"></field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
|
||||
<function model="project.phase" name="set_open" eval="[ref('project_phase_1')]"/>
|
||||
|
||||
<record id="project_phase_2" model="project.phase">
|
||||
<field eval="[(6, 0, [ref('project_phase_1')])]" name="previous_phase_ids"/>
|
||||
<field name="name">Design Model</field>
|
||||
<field name="product_uom" ref="product.product_uom_day"/>
|
||||
<field eval="2" name="sequence"/>
|
||||
<field name="duration">20</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
<function model="project.phase" name="set_open" eval="[ref('project_phase_2')]"/>
|
||||
|
||||
<record id="project_phase_3" model="project.phase">
|
||||
<field eval="[(6, 0, [ref('project_phase_2')])]" name="previous_phase_ids"/>
|
||||
<field name="name">Planning and compute Risk analysis, Time chart </field>
|
||||
<field name="product_uom" ref="product.product_uom_day"/>
|
||||
<field eval="3" name="sequence"/>
|
||||
<field name="duration">20</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
<function model="project.phase" name="set_open" eval="[ref('project_phase_3')]"/>
|
||||
|
||||
<record id="project_phase_4" model="project.phase">
|
||||
<field eval="[(6, 0, [ref('project_phase_3')])]" name="previous_phase_ids"/>
|
||||
<field name="name">Development and Integration</field>
|
||||
<field name="product_uom" ref="product.product_uom_day"/>
|
||||
<field eval="4" name="sequence"/>
|
||||
<field name="duration">90</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
|
||||
<record id="project_phase_5" model="project.phase">
|
||||
<field eval="[(6, 0, [ref('project_phase_4')])]" name="previous_phase_ids"/>
|
||||
<field name="name">Review and Testing</field>
|
||||
<field name="product_uom" ref="product.product_uom_day"/>
|
||||
<field eval="5" name="sequence"/>
|
||||
<field name="duration">30</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
|
||||
<record id="project_phase_6" model="project.phase">
|
||||
<field eval="[(6, 0, [ref('project_phase_5')])]" name="previous_phase_ids"/>
|
||||
<field name="name">Deployement and Training</field>
|
||||
<field name="product_uom" ref="product.product_uom_day"/>
|
||||
<field eval="6" name="sequence"/>
|
||||
<field name="duration">10</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
|
||||
<function model="project.phase" name="set_open" eval="[ref('project_phase_6')]"/>
|
||||
<!-- Tasks -->
|
||||
|
||||
<record id="project.project_task_1" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_1"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_2" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_1"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_3" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_2"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_4" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_3"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_5" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_4"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_6" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_5"/>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_7" model="project.task">
|
||||
<field name="phase_id" ref="project_phase_6"/>
|
||||
</record>
|
||||
|
||||
<!-- run scheduling of phase -->
|
||||
<!--<function model="project.project" name="schedule_phases" eval="(ref('project.project_project_1'),)"/>-->
|
||||
<!-- run scheduling of tasks -->
|
||||
<!--<function model="project.project" name="schedule_tasks" eval="(ref('project.project_project_1'),)"/>-->
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<report auto="False" id="report_project_task_gantt" model="project.task" name="project.tasks.gantt" string="Gantt Representation"/>
|
||||
<report auto="False" id="report_project_project_gantt" model="project.project" name="project.project.gantt" string="Gantt Representation"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,387 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<menuitem id="base.menu_project_long_term" name="Long Term Planning" parent="base.menu_main_pm" sequence="3"/>
|
||||
|
||||
<!-- Project User Allocation -->
|
||||
<record id="view_project_user_allocation_gantt" model="ir.ui.view">
|
||||
<field name="name">project.user.allocation.gantt</field>
|
||||
<field name="model">project.user.allocation</field>
|
||||
<field name="arch" type="xml">
|
||||
<gantt date_start="date_start" date_stop="date_end" default_group_by="user_id">
|
||||
</gantt>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_user_allocation_calendar" model="ir.ui.view">
|
||||
<field name="name">project.user.allocation.calendar</field>
|
||||
<field name="model">project.user.allocation</field>
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<calendar color="user_id" date_start="date_start" date_stop="date_end" day_length="12" string="Users">
|
||||
<field name="phase_id"/>
|
||||
<field name="project_id"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_project_user_allocation_form" model="ir.ui.view">
|
||||
<field name="name">project.user.allocation.form</field>
|
||||
<field name="model">project.user.allocation</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project User Allocation" version="7.0">
|
||||
<group col="4">
|
||||
<field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/>
|
||||
<field name="phase_id"/>
|
||||
<field name="project_id"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_user_allocation_list" model="ir.ui.view">
|
||||
<field name="name">project.user.allocation.list</field>
|
||||
<field name="model">project.user.allocation</field>
|
||||
<field name="priority" eval="5"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom" string="Project User Allocation">
|
||||
<field name="user_id" context="{'default_groups_ref': ['base.group_user', 'project.group_project_user']}"/>
|
||||
<field name="phase_id"/>
|
||||
<field name="project_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_user_allocation_search" model="ir.ui.view">
|
||||
<field name="name">project.user.allocation.search</field>
|
||||
<field name="model">project.user.allocation</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Team Planning">
|
||||
<field name="user_id"/>
|
||||
<field name="phase_id"/>
|
||||
<field name="project_id"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter name="user" string="User" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
|
||||
<filter string="Project" icon="terp-folder-violet" domain="[]" context="{'group_by':'project_id'}"/>
|
||||
<filter string="Phase" icon="terp-project" domain="[]" context="{'group_by':'phase_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="act_resouce_allocation" model="ir.actions.act_window">
|
||||
<field name="name">Team Planning</field>
|
||||
<field name="res_model">project.user.allocation</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">gantt,tree,form,calendar</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="search_view_id" ref="view_project_user_allocation_search"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Project Phase -->
|
||||
|
||||
<record id="act_project_phases" model="ir.actions.act_window">
|
||||
<field name="res_model">project.phase</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="name">Phases</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_project_id': [active_id], 'default_project_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
<record id="project_phase_form" model="ir.ui.view">
|
||||
<field name="name">Inherit project form : Phase</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='options_active']" position='inside'>
|
||||
<field name="use_phases" class="oe_inline"/>
|
||||
<label for="use_phases"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='buttons']" position='inside'>
|
||||
<button name="%(act_project_phases)d"
|
||||
string="Phases" type="action"
|
||||
attrs="{'invisible':[('use_phases','=', 0)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_kanban_phase" model="ir.ui.view">
|
||||
<field name="name">project.project.kanban.inherited</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.view_project_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="use_tasks" position="after">
|
||||
<field name="use_phases"/>
|
||||
<field name="phase_count"/>
|
||||
</field>
|
||||
<xpath expr="//div[contains(@class, 'oe_kanban_project_list')]" position="inside">
|
||||
<a t-if="record.use_phases.raw_value"
|
||||
name="%(act_project_phases)d" type="action"
|
||||
groups="base.group_user">
|
||||
<span t-if="record.phase_count.raw_value gt 1"><field name="phase_count"/> Phases</span>
|
||||
<span t-if="record.phase_count.raw_value lt 2"><field name="phase_count"/> Phase</span>
|
||||
</a>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_phase_task_list" model="ir.actions.act_window">
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="name">Tasks</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_phase_id': [active_id], 'default_phase_id' : active_id}</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_phase_form" model="ir.ui.view">
|
||||
<field name="name">project.phase.form</field>
|
||||
<field name="model">project.phase</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project Phase" version="7.0">
|
||||
<header>
|
||||
<button string="Start Phase" name="set_open" states="pending,draft" class="oe_highlight"/>
|
||||
<button string="Done" name="set_done" states="pending,open"/>
|
||||
<button string="Pending" name="set_pending" states="open"/>
|
||||
<button string="Draft" name="set_draft" states="open"/>
|
||||
<button string="Cancel Phase" name="set_cancel" states="draft,open,pending"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,open,done" statusbar_colors='{"pending":"blue"}'/>
|
||||
</header>
|
||||
<sheet>
|
||||
<button name="%(project_phase_task_list)d" string="Related Tasks" type="action" class="oe_right"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<label for="date_start" string="Duration"/>
|
||||
<div>
|
||||
<div>
|
||||
<field name="duration" class="oe_inline"/>
|
||||
<field name="product_uom" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="date_start" class="oe_inline"/><label string=" - " class="oe_inline"/><field name="date_end" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="project_id" on_change="onchange_project(project_id)"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Team Planning">
|
||||
<field name="user_ids">
|
||||
<tree editable="bottom" string="Project Users">
|
||||
<field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</tree>
|
||||
<form string="Project Users" version="7.0">
|
||||
<group col="4">
|
||||
<field name="user_id"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Tasks Details">
|
||||
<field name="task_ids" readonly="1" context="{'default_project_id' :project_id}">
|
||||
<tree string="Project's Tasks">
|
||||
<field name="sequence"/>
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="planned_hours" widget="float_time"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="total_hours" sum='Total Hours'/>
|
||||
<field name="remaining_hours" widget="float_time" sum="Remaining Hours"/>
|
||||
<field name="stage_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Constraints">
|
||||
<group>
|
||||
<group>
|
||||
<field name="constraint_date_start"/>
|
||||
<field name="constraint_date_end"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="user_force_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Previous Phases"/>
|
||||
<field name="previous_phase_ids"/>
|
||||
<separator string="Next Phases"/>
|
||||
<field name="next_phase_ids"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_phase_list" model="ir.ui.view">
|
||||
<field name="name">project.phase.list</field>
|
||||
<field name="model">project.phase</field>
|
||||
<field name="priority" eval="5"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree colors="grey:state in ('cancelled','done');blue:state == 'pending'" string="Project Phases">
|
||||
<field name="name"/>
|
||||
<field name="project_id" on_change="onchange_project(project_id)"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="duration"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_phase_calendar" model="ir.ui.view">
|
||||
<field name="name">project.phase.calendar</field>
|
||||
<field name="model">project.phase</field>
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<calendar color="project_id" date_start="date_start" date_stop="date_end" day_length="12">
|
||||
<field name="name"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_phase_gantt" model="ir.ui.view">
|
||||
<field name="name">project.phase.gantt</field>
|
||||
<field name="model">project.phase</field>
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<gantt date_stop="date_end" date_start="date_start" default_group_by="project_id">
|
||||
</gantt>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_phase_search" model="ir.ui.view">
|
||||
<field name="name">project.phase.search</field>
|
||||
<field name="model">project.phase</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Project Phases">
|
||||
<field name="name" string="Project Phases"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<filter string="New" domain="[('state','=','draft')]" name="current" help="New Phases" icon="terp-check"/>
|
||||
<filter string="In Progress" name="Progress" domain="[('state','=','open')]" help="In Progress Phases" icon="terp-camera_test"/>
|
||||
<filter string="Pending" domain="[('state','=','pending')]" help="Pending Phases" icon="terp-gtk-media-pause"/>
|
||||
<separator/>
|
||||
<filter string="My Projects" domain="[('project_id.user_id','=',uid)]" help="My Projects" icon="terp-folder-violet"/>
|
||||
<field name="project_id"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Project" icon="terp-folder-violet" domain="[]" context="{'group_by':'project_id'}" name="project"/>
|
||||
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
|
||||
<filter string="Start Month" icon="terp-go-month" domain="[]" context="{'group_by':'date_start'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_project_phase" model="ir.actions.act_window">
|
||||
<field name="name">Project Phases</field>
|
||||
<field name="res_model">project.phase</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">gantt,tree,form,calendar</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="search_view_id" ref="view_project_phase_search"/>
|
||||
<field name="help">A project can be split into the different phases. For each phase, you can define your users allocation, describe different tasks and link your phase to previous and next phases, add date constraints for the automated scheduling. Use the long term planning in order to planify your available users, convert your phases into a series of tasks when you start working on the project.</field>
|
||||
</record>
|
||||
|
||||
<record id="act_project_phase_list" model="ir.actions.act_window">
|
||||
<field name="name">Project Phases</field>
|
||||
<field name="res_model">project.phase</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form,calendar</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="search_view_id" ref="view_project_phase_search"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Project Task -->
|
||||
<record id="view_phase_task_form2" model="ir.ui.view">
|
||||
<field name="name">phase.task.form2</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='user_id']" position="after">
|
||||
<field name="phase_id" context="{'default_project_id' : project_id}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_phase_task_search_form" model="ir.ui.view">
|
||||
<field name="name">phase.task.search.form</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_search_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="user_id" position="before">
|
||||
<field name="phase_id" domain="[]"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_phase_task_search_form_group" model="ir.ui.view">
|
||||
<field name="name">phase.task.search.form.group</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_search_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter string="Project" name="group_project_id" icon="terp-folder-violet" domain="[]" context="{'group_by':'project_id'}" position="after">
|
||||
<filter string="Project Phase" name="group_project_id_phase" icon="terp-folder-violet" domain="[]" context="{'group_by':'phase_id'}"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_phase_task_search_form_tree" model="ir.ui.view">
|
||||
<field name="name">phase.task.search.form.tree</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_tree2"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="project_id" position="after">
|
||||
<field name="phase_id" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="analytic_account_inherited_phase_form" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.phase.form.inherit</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//div[@name="project"]' position='inside'>
|
||||
<field name="use_phases"/>
|
||||
<label for="use_phases"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<menuitem action="act_project_phase_list"
|
||||
id="menu_project_phase_list" parent="project.menu_project_management" sequence="2"/>
|
||||
|
||||
<menuitem action="act_project_phase"
|
||||
icon="STOCK_INDENT"
|
||||
id="menu_project_phase" parent="base.menu_project_long_term" sequence="1"/>
|
||||
|
||||
<menuitem id="menu_resouce_allocation" action="act_resouce_allocation"
|
||||
icon="STOCK_INDENT"
|
||||
parent="base.menu_project_long_term" sequence="2"/>
|
||||
|
||||
<menuitem id="menu_pm_users_project1"
|
||||
name="Resources" parent="base.menu_definitions" sequence="3"/>
|
||||
|
||||
<menuitem id="menu_phase_schedule" name="Scheduling" parent="base.menu_main_pm" sequence="4" groups="project.group_project_user,project.group_project_manager"/>
|
||||
<menuitem action="resource.action_resource_resource_tree" id="menu_view_resource" parent="menu_pm_users_project1" sequence="2"/>
|
||||
<menuitem action="resource.action_resource_calendar_form" id="menu_view_resource_calendar" parent="menu_pm_users_project1" sequence="5"/>
|
||||
<menuitem action="resource.action_resource_calendar_leave_tree" id="menu_view_resource_calendar_leaves" parent="menu_pm_users_project1" sequence="3"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,110 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="wkf_phase" model="workflow">
|
||||
<field name="name">project.phase.wkf</field>
|
||||
<field name="osv">project.phase</field>
|
||||
<field name="on_create">True</field>
|
||||
</record>
|
||||
|
||||
<record id="act_draft" model="workflow.activity">
|
||||
<field name="wkf_id" ref="wkf_phase"/>
|
||||
<field name="flow_start">True</field>
|
||||
<field name="name">set_draft</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">set_draft()</field>
|
||||
</record>
|
||||
|
||||
<record id="act_start_phase" model="workflow.activity">
|
||||
<field name="wkf_id" ref="wkf_phase"/>
|
||||
<field name="name">set_open</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">set_open()</field>
|
||||
</record>
|
||||
|
||||
<record id="act_cancel_phase" model="workflow.activity">
|
||||
<field name="wkf_id" ref="wkf_phase"/>
|
||||
<field name="name">set_cancel</field>
|
||||
<field name="flow_stop">True</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">set_cancel()</field>
|
||||
</record>
|
||||
|
||||
<record id="act_phase_pending" model="workflow.activity">
|
||||
<field name="wkf_id" ref="wkf_phase"/>
|
||||
<field name="name">set_pending</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">set_pending()</field>
|
||||
</record>
|
||||
|
||||
<record id="act_phase_done" model="workflow.activity">
|
||||
<field name="wkf_id" ref="wkf_phase"/>
|
||||
<field name="name">set_done</field>
|
||||
<field name="flow_stop">True</field>
|
||||
<field name="kind">function</field>
|
||||
<field name="action">set_done()</field>
|
||||
</record>
|
||||
|
||||
<record id="t0" model="workflow.transition">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_start_phase"/>
|
||||
<field name="signal">set_open</field>
|
||||
</record>
|
||||
|
||||
<record id="t1" model="workflow.transition">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_cancel_phase"/>
|
||||
<field name="signal">set_cancel</field>
|
||||
</record>
|
||||
|
||||
<record id="t2" model="workflow.transition">
|
||||
<field name="act_from" ref="act_draft"/>
|
||||
<field name="act_to" ref="act_phase_done"/>
|
||||
<field name="signal">set_done</field>
|
||||
</record>
|
||||
|
||||
<record id="t3" model="workflow.transition">
|
||||
<field name="act_from" ref="act_start_phase"/>
|
||||
<field name="act_to" ref="act_phase_pending"/>
|
||||
<field name="signal">set_pending</field>
|
||||
</record>
|
||||
|
||||
<record id="t4" model="workflow.transition">
|
||||
<field name="act_from" ref="act_phase_pending"/>
|
||||
<field name="act_to" ref="act_cancel_phase"/>
|
||||
<field name="signal">set_cancel</field>
|
||||
</record>
|
||||
|
||||
<record id="t5" model="workflow.transition">
|
||||
<field name="act_from" ref="act_phase_pending"/>
|
||||
<field name="act_to" ref="act_draft"/>
|
||||
<field name="signal">set_draft</field>
|
||||
</record>
|
||||
|
||||
<record id="t6" model="workflow.transition">
|
||||
<field name="act_from" ref="act_phase_pending"/>
|
||||
<field name="act_to" ref="act_start_phase"/>
|
||||
<field name="signal">set_open</field>
|
||||
</record>
|
||||
|
||||
<record id="t7" model="workflow.transition">
|
||||
<field name="act_from" ref="act_start_phase"/>
|
||||
<field name="act_to" ref="act_cancel_phase"/>
|
||||
<field name="signal">set_cancel</field>
|
||||
</record>
|
||||
|
||||
<record id="t8" model="workflow.transition">
|
||||
<field name="act_from" ref="act_start_phase"/>
|
||||
<field name="act_to" ref="act_phase_done"/>
|
||||
<field name="signal">set_done</field>
|
||||
</record>
|
||||
|
||||
<record id="t9" model="workflow.transition">
|
||||
<field name="act_from" ref="act_start_phase"/>
|
||||
<field name="act_to" ref="act_draft"/>
|
||||
<field name="signal">set_draft</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,9 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_project_phase,project.phase,model_project_phase,project.group_project_user,1,1,1,0
|
||||
access_project_user_allocation,project.user.allocation,model_project_user_allocation,project.group_project_user,1,0,0,0
|
||||
access_project_phase_manager,project.phase manager,model_project_phase,project.group_project_manager,1,1,1,1
|
||||
access_project_user_allocation_manager,project.user.allocation manager,model_project_user_allocation,project.group_project_manager,1,1,1,1
|
||||
access_resource_resource_user,user.user user,resource.model_resource_resource,project.group_project_user,1,0,0,0
|
||||
access_resource_resource_manager,user.user manager,resource.model_resource_resource,project.group_project_manager,1,1,1,1
|
||||
access_project_user_allocation_manager,project.user.allocation.manager,model_project_user_allocation,project.group_project_manager,1,1,1,1
|
||||
access_project_resource_calendar_attendance,resource.calendar.attendance,resource.model_resource_calendar_attendance,project.group_project_manager,1,0,0,0
|
|
|
@ -1,79 +0,0 @@
|
|||
-
|
||||
In order to test process of Phases,
|
||||
-
|
||||
I create a record to schedule the phase of project.
|
||||
-
|
||||
!record {model: project.compute.phases, id: project_compute_phases01}:
|
||||
target_project: 'one'
|
||||
project_id: project.project_project_1
|
||||
-
|
||||
I schedule the phases.
|
||||
-
|
||||
!python {model: project.compute.phases}: |
|
||||
self.check_selection(cr, uid, [ref("project_compute_phases01")])
|
||||
-
|
||||
I check the starting date and ending date on the phases after scheduling.
|
||||
-
|
||||
!python {model: project.project}: |
|
||||
project = self.browse(cr, uid, ref("project.project_project_1"), context=context)
|
||||
def _convert(date):
|
||||
import time
|
||||
return time.strptime(date, '%Y-%m-%d %H:%M:%S')
|
||||
def _check(phase, _convert, _check): #TOFIX: why need to pass function ?
|
||||
for next_phase in phase.next_phase_ids:
|
||||
assert _convert(next_phase.date_start) >= _convert(phase.date_end), "Phase does not start in proper date."
|
||||
_check(next_phase, _convert, _check)
|
||||
return True
|
||||
|
||||
for phase in project.phase_ids:
|
||||
|
||||
assert phase.date_start, "Start date should be computed."
|
||||
assert phase.date_end, "End date should be computed."
|
||||
if not phase.previous_phase_ids and phase.constraint_date_start:
|
||||
assert _convert(phase.date_start) >= _convert(phase.constraint_date_start), "Phase does not start in proper date."
|
||||
_check(phase, _convert, _check)
|
||||
-
|
||||
I open phase.
|
||||
-
|
||||
!python {model: project.phase}: |
|
||||
self.set_open(cr, uid, [ref("project_phase_1")])
|
||||
-
|
||||
I check state of phase after opened.
|
||||
-
|
||||
!assert {model: project.phase, id: project_phase_1, severity: error, string: Phase should be in open state}:
|
||||
- state == "open"
|
||||
-
|
||||
I put phase in pending state.
|
||||
-
|
||||
!python {model: project.phase}: |
|
||||
self.set_pending(cr, uid, [ref("project_phase_1")])
|
||||
-
|
||||
I check state of phase after put in pending.
|
||||
-
|
||||
!assert {model: project.phase, id: project_phase_1, severity: error, string: Phase should be in pending state}:
|
||||
- state == "pending"
|
||||
-
|
||||
I make Phase in cancel state.
|
||||
-
|
||||
!python {model: project.phase}: |
|
||||
self.set_cancel(cr, uid, [ref("project_phase_1")])
|
||||
-
|
||||
I check state of phase after cancelled.
|
||||
-
|
||||
!assert {model: project.phase, id: project_phase_1, severity: error, string: Phase should be in cancel state}:
|
||||
- state == "cancelled"
|
||||
-
|
||||
I put again in draft phase.
|
||||
-
|
||||
!python {model: project.phase}: |
|
||||
self.set_draft(cr, uid, [ref("project_phase_1")])
|
||||
-
|
||||
I close phase.
|
||||
-
|
||||
!python {model: project.phase}: |
|
||||
self.set_done(cr, uid, [ref("project_phase_1")])
|
||||
-
|
||||
I check state of phase after closed.
|
||||
-
|
||||
!assert {model: project.phase, id: project_phase_1, severity: error, string: Phase should be in done state}:
|
||||
- state == "done"
|
|
@ -1,19 +0,0 @@
|
|||
-
|
||||
I create a record to compute the tasks of project.
|
||||
-
|
||||
!record {model: project.compute.tasks, id: project_compute_tasks0}:
|
||||
project_id: project.project_project_1
|
||||
-
|
||||
I compute and shedule the tasks.
|
||||
-
|
||||
!python {model: project.compute.tasks}: |
|
||||
self.compute_date(cr, uid, [ref("project_compute_tasks0")])
|
||||
-
|
||||
Check if tasks scheduled, check that either of task's start_date, end_date and user_id is not null
|
||||
-
|
||||
!python {model: project.project}: |
|
||||
prj = self.browse(cr, uid, [ref("project.project_project_1")])[0]
|
||||
for task in prj.tasks:
|
||||
if task.stage_id and task.stage_id.fold:
|
||||
continue
|
||||
assert task.user_id and task.date_start and task.date_end, "Project tasks not scheduled"
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-s
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import project_compute_phases
|
||||
import project_compute_tasks
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,76 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from openerp.tools.translate import _
|
||||
from openerp.osv import fields, osv
|
||||
|
||||
class project_compute_phases(osv.osv_memory):
|
||||
_name = 'project.compute.phases'
|
||||
_description = 'Project Compute Phases'
|
||||
_columns = {
|
||||
'target_project': fields.selection([
|
||||
('all', 'Compute All My Projects'),
|
||||
('one', 'Compute a Single Project'),
|
||||
], 'Action', required=True),
|
||||
'project_id': fields.many2one('project.project', 'Project')
|
||||
}
|
||||
_defaults = {
|
||||
'target_project': 'one'
|
||||
}
|
||||
|
||||
def check_selection(self, cr, uid, ids, context=None):
|
||||
return self.compute_date(cr, uid, ids, context=context)
|
||||
|
||||
def compute_date(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Compute the phases for scheduling.
|
||||
"""
|
||||
project_pool = self.pool.get('project.project')
|
||||
data = self.read(cr, uid, ids, [], context=context)[0]
|
||||
if not data['project_id'] and data['target_project'] == 'one':
|
||||
raise osv.except_osv(_('Error!'), _('Please specify a project to schedule.'))
|
||||
|
||||
if data['target_project'] == 'one':
|
||||
project_ids = [data['project_id'][0]]
|
||||
else:
|
||||
project_ids = project_pool.search(cr, uid, [('user_id','=',uid)], context=context)
|
||||
|
||||
if project_ids:
|
||||
project_pool.schedule_phases(cr, uid, project_ids, context=context)
|
||||
return self._open_phases_list(cr, uid, data, context=context)
|
||||
|
||||
def _open_phases_list(self, cr, uid, data, context=None):
|
||||
"""
|
||||
Return the scheduled phases list.
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
mod_obj = self.pool.get('ir.model.data')
|
||||
act_obj = self.pool.get('ir.actions.act_window')
|
||||
result = mod_obj._get_id(cr, uid, 'project_long_term', 'act_project_phase')
|
||||
id = mod_obj.read(cr, uid, [result], ['res_id'])[0]['res_id']
|
||||
result = act_obj.read(cr, uid, [id], context=context)[0]
|
||||
result['target'] = 'current'
|
||||
project_id = data.get('project_id') and data.get('project_id')[0] or False
|
||||
result['context'] = {"search_default_project_id":project_id, "default_project_id":project_id, "search_default_current": 1}
|
||||
return result
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,39 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_project_compute_phases_select" model="ir.ui.view">
|
||||
<field name="name">Schedule Phases</field>
|
||||
<field name="model">project.compute.phases</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Schedule Phases" version="7.0">
|
||||
<group>
|
||||
<field name="target_project"/>
|
||||
<field name="project_id" attrs="{'invisible':[('target_project','=','all')], 'required':[('target_project','!=','all')]}"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="check_selection" string="C_ompute" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_compute_phases" model="ir.actions.act_window">
|
||||
<field name="name">Schedule Phases</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.compute.phases</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_project_compute_phases_select"/>
|
||||
<field name="target">new</field>
|
||||
<field name="help">To schedule phases of all or a specified project. It then open a gantt view.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_compute_phase"
|
||||
parent="menu_phase_schedule" action="action_project_compute_phases"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,63 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
|
||||
class project_compute_tasks(osv.osv_memory):
|
||||
_name = 'project.compute.tasks'
|
||||
_description = 'Project Compute Tasks'
|
||||
_columns = {
|
||||
'project_id': fields.many2one('project.project', 'Project', required=True)
|
||||
}
|
||||
|
||||
def compute_date(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Schedule the tasks according to users and priority.
|
||||
"""
|
||||
project_pool = self.pool.get('project.project')
|
||||
task_pool = self.pool.get('project.task')
|
||||
if context is None:
|
||||
context = {}
|
||||
context['compute_by'] = 'project'
|
||||
data = self.read(cr, uid, ids, [])[0]
|
||||
project_id = data['project_id'][0]
|
||||
project_pool.schedule_tasks(cr, uid, [project_id], context=context)
|
||||
return self._open_task_list(cr, uid, data, context=context)
|
||||
|
||||
def _open_task_list(self, cr, uid, data, context=None):
|
||||
"""
|
||||
Return the scheduled task list.
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
mod_obj = self.pool.get('ir.model.data')
|
||||
act_obj = self.pool.get('ir.actions.act_window')
|
||||
result = mod_obj._get_id(cr, uid, 'project_long_term', 'act_resouce_allocation')
|
||||
id = mod_obj.read(cr, uid, [result], ['res_id'])[0]['res_id']
|
||||
result = {}
|
||||
if not id:
|
||||
return result
|
||||
result = act_obj.read(cr, uid, [id], context=context)[0]
|
||||
result['target'] = 'current'
|
||||
return result
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_project_compute_tasks" model="ir.ui.view">
|
||||
<field name="name">Schedule Tasks</field>
|
||||
<field name="model">project.compute.tasks</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Schedule Tasks" version="7.0">
|
||||
<group>
|
||||
<field name="project_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="compute_date" string="C_ompute" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_compute_tasks" model="ir.actions.act_window">
|
||||
<field name="name">Schedule Tasks</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.compute.tasks</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_project_compute_tasks"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_compute_tasks"
|
||||
parent="menu_phase_schedule" action="action_project_compute_tasks"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -9,20 +9,6 @@
|
|||
|
||||
<!-- Report for Users' Timesheet and Task Hours per Month -->
|
||||
|
||||
<record id="view_report_timesheet_task_user_tree" model="ir.ui.view">
|
||||
<field name="name">report.timesheet.task.user.tree</field>
|
||||
<field name="model">report.timesheet.task.user</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Timesheet/Task hours Report Per Month" >
|
||||
<field name="name"/>
|
||||
<field name="year" invisible="1"/>
|
||||
<field name="month" invisible="1"/>
|
||||
<field name="user_id"/>
|
||||
<field name="timesheet_hrs" widget="float_time" />
|
||||
<field name="task_hrs" widget="float_time"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_report_timesheet_task_user_search" model="ir.ui.view">
|
||||
<field name="name">report.timesheet.task.user.search</field>
|
||||
<field name="model">report.timesheet.task.user</field>
|
||||
|
@ -55,7 +41,7 @@
|
|||
<field name="name">Task Hours Per Month</field>
|
||||
<field name="res_model">report.timesheet.task.user</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,graph</field>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="context">{'search_default_year':1,'search_default_month':1, 'search_default_group_user_id':1}</field>
|
||||
</record>
|
||||
<menuitem id="menu_timesheet_task_user" parent="hr.menu_hr_reporting_timesheet"
|
||||
|
|
|
@ -2506,7 +2506,7 @@ class stock_move(osv.osv):
|
|||
source_location = move.location_dest_id
|
||||
if source_location.usage != 'internal':
|
||||
#restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
|
||||
raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
|
||||
raise osv.except_osv(_('Error!'), _('Operation Forbidden! it is not allowed to scrap products from a virtual location: %s' %(move.location_id.complete_name)))
|
||||
move_qty = move.product_qty
|
||||
uos_qty = quantity / move_qty * move.product_uos_qty
|
||||
default_val = {
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
.tour-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1009;
|
||||
background-color: #000;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tour-step-backdrop {
|
||||
position: relative;
|
||||
z-index: 1011;
|
||||
}
|
||||
.tour-step-background {
|
||||
position: absolute;
|
||||
z-index: 1010;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.popover[class*="tour-"] .popover-navigation {
|
||||
padding: 9px 14px;
|
||||
}
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=end] {
|
||||
float: right;
|
||||
}
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=prev],
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=next],
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=end] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=prev].disabled,
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=next].disabled,
|
||||
.popover[class*="tour-"] .popover-navigation *[data-role=end].disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.popover[class*="tour-"].orphan {
|
||||
position: fixed;
|
||||
margin-top: 0;
|
||||
}
|
||||
.popover[class*="tour-"].orphan .arrow {
|
||||
display: none;
|
||||
}
|
|
@ -1,559 +0,0 @@
|
|||
/* ===========================================================
|
||||
# bootstrap-tour - v0.6.1
|
||||
# http://bootstraptour.com
|
||||
# ==============================================================
|
||||
# Copyright 2012-2013 Ulrich Sossou
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
(function($, window) {
|
||||
var Tour, document;
|
||||
document = window.document;
|
||||
Tour = (function() {
|
||||
function Tour(options) {
|
||||
this._options = $.extend({
|
||||
name: "tour",
|
||||
container: "body",
|
||||
keyboard: true,
|
||||
storage: window.localStorage,
|
||||
debug: false,
|
||||
backdrop: false,
|
||||
redirect: true,
|
||||
orphan: false,
|
||||
basePath: "",
|
||||
template: "<div class='popover'> <div class='arrow'></div> <h3 class='popover-title'></h3> <div class='popover-content'></div> <nav class='popover-navigation'> <div class='btn-group'> <button class='btn btn-sm btn-default' data-role='prev'>« Prev</button> <button class='btn btn-sm btn-default' data-role='next'>Next »</button> </div> <button class='btn btn-sm btn-default' data-role='end'>End tour</button> </nav> </div>",
|
||||
afterSetState: function(key, value) {},
|
||||
afterGetState: function(key, value) {},
|
||||
afterRemoveState: function(key) {},
|
||||
onStart: function(tour) {},
|
||||
onEnd: function(tour) {},
|
||||
onShow: function(tour) {},
|
||||
onShown: function(tour) {},
|
||||
onHide: function(tour) {},
|
||||
onHidden: function(tour) {},
|
||||
onNext: function(tour) {},
|
||||
onPrev: function(tour) {}
|
||||
}, options);
|
||||
this._steps = [];
|
||||
this.setCurrentStep();
|
||||
this.backdrop = {
|
||||
overlay: null,
|
||||
$element: null,
|
||||
$background: null
|
||||
};
|
||||
}
|
||||
|
||||
Tour.prototype.setState = function(key, value) {
|
||||
var keyName;
|
||||
if (this._options.storage) {
|
||||
keyName = "" + this._options.name + "_" + key;
|
||||
this._options.storage.setItem(keyName, value);
|
||||
return this._options.afterSetState(keyName, value);
|
||||
} else {
|
||||
if (this._state == null) {
|
||||
this._state = {};
|
||||
}
|
||||
return this._state[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype.removeState = function(key) {
|
||||
var keyName;
|
||||
if (this._options.storage) {
|
||||
keyName = "" + this._options.name + "_" + key;
|
||||
this._options.storage.removeItem(keyName);
|
||||
return this._options.afterRemoveState(keyName);
|
||||
} else {
|
||||
if (this._state != null) {
|
||||
return delete this._state[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype.getState = function(key) {
|
||||
var keyName, value;
|
||||
if (this._options.storage) {
|
||||
keyName = "" + this._options.name + "_" + key;
|
||||
value = this._options.storage.getItem(keyName);
|
||||
} else {
|
||||
if (this._state != null) {
|
||||
value = this._state[key];
|
||||
}
|
||||
}
|
||||
if (value === void 0 || value === "null") {
|
||||
value = null;
|
||||
}
|
||||
this._options.afterGetState(key, value);
|
||||
return value;
|
||||
};
|
||||
|
||||
Tour.prototype.addSteps = function(steps) {
|
||||
var step, _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = steps.length; _i < _len; _i++) {
|
||||
step = steps[_i];
|
||||
_results.push(this.addStep(step));
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Tour.prototype.addStep = function(step) {
|
||||
return this._steps.push(step);
|
||||
};
|
||||
|
||||
Tour.prototype.getStep = function(i) {
|
||||
if (this._steps[i] != null) {
|
||||
return $.extend({
|
||||
id: "step-" + i,
|
||||
path: "",
|
||||
placement: "right",
|
||||
title: "",
|
||||
content: "<p></p>",
|
||||
next: i === this._steps.length - 1 ? -1 : i + 1,
|
||||
prev: i - 1,
|
||||
animation: true,
|
||||
container: this._options.container,
|
||||
backdrop: this._options.backdrop,
|
||||
redirect: this._options.redirect,
|
||||
orphan: this._options.orphan,
|
||||
template: this._options.template,
|
||||
onShow: this._options.onShow,
|
||||
onShown: this._options.onShown,
|
||||
onHide: this._options.onHide,
|
||||
onHidden: this._options.onHidden,
|
||||
onNext: this._options.onNext,
|
||||
onPrev: this._options.onPrev
|
||||
}, this._steps[i]);
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype.start = function(force) {
|
||||
var promise,
|
||||
_this = this;
|
||||
if (force == null) {
|
||||
force = false;
|
||||
}
|
||||
if (this.ended() && !force) {
|
||||
return this._debug("Tour ended, start prevented.");
|
||||
}
|
||||
$(document).off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role=next]").on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role=next]:not(.disabled)", function(e) {
|
||||
e.preventDefault();
|
||||
return _this.next();
|
||||
});
|
||||
$(document).off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role=prev]").on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role=prev]:not(.disabled)", function(e) {
|
||||
e.preventDefault();
|
||||
return _this.prev();
|
||||
});
|
||||
$(document).off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role=end]").on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role=end]", function(e) {
|
||||
e.preventDefault();
|
||||
return _this.end();
|
||||
});
|
||||
this._onResize(function() {
|
||||
return _this.showStep(_this._current);
|
||||
});
|
||||
this._setupKeyboardNavigation();
|
||||
promise = this._makePromise(this._options.onStart != null ? this._options.onStart(this) : void 0);
|
||||
return this._callOnPromiseDone(promise, this.showStep, this._current);
|
||||
};
|
||||
|
||||
Tour.prototype.next = function() {
|
||||
var promise;
|
||||
if (this.ended()) {
|
||||
return this._debug("Tour ended, next prevented.");
|
||||
}
|
||||
promise = this.hideStep(this._current);
|
||||
return this._callOnPromiseDone(promise, this._showNextStep);
|
||||
};
|
||||
|
||||
Tour.prototype.prev = function() {
|
||||
var promise;
|
||||
if (this.ended()) {
|
||||
return this._debug("Tour ended, prev prevented.");
|
||||
}
|
||||
promise = this.hideStep(this._current);
|
||||
return this._callOnPromiseDone(promise, this._showPrevStep);
|
||||
};
|
||||
|
||||
Tour.prototype.goto = function(i) {
|
||||
var promise;
|
||||
if (this.ended()) {
|
||||
return this._debug("Tour ended, goto prevented.");
|
||||
}
|
||||
promise = this.hideStep(this._current);
|
||||
return this._callOnPromiseDone(promise, this.showStep, i);
|
||||
};
|
||||
|
||||
Tour.prototype.end = function() {
|
||||
var endHelper, hidePromise,
|
||||
_this = this;
|
||||
endHelper = function(e) {
|
||||
$(document).off("click.tour-" + _this._options.name);
|
||||
$(document).off("keyup.tour-" + _this._options.name);
|
||||
$(window).off("resize.tour-" + _this._options.name);
|
||||
_this.setState("end", "yes");
|
||||
if (_this._options.onEnd != null) {
|
||||
return _this._options.onEnd(_this);
|
||||
}
|
||||
};
|
||||
hidePromise = this.hideStep(this._current);
|
||||
return this._callOnPromiseDone(hidePromise, endHelper);
|
||||
};
|
||||
|
||||
Tour.prototype.ended = function() {
|
||||
return !!this.getState("end");
|
||||
};
|
||||
|
||||
Tour.prototype.restart = function() {
|
||||
this.removeState("current_step");
|
||||
this.removeState("end");
|
||||
this.setCurrentStep(0);
|
||||
return this.start();
|
||||
};
|
||||
|
||||
Tour.prototype.hideStep = function(i) {
|
||||
var hideStepHelper, promise, step,
|
||||
_this = this;
|
||||
step = this.getStep(i);
|
||||
promise = this._makePromise(step.onHide != null ? step.onHide(this, i) : void 0);
|
||||
hideStepHelper = function(e) {
|
||||
var $element;
|
||||
$element = _this._isOrphan(step) ? $("body") : $(step.element);
|
||||
$element.popover("destroy");
|
||||
if (step.reflex) {
|
||||
$element.css("cursor", "").off("click.tour-" + _this._options.name);
|
||||
}
|
||||
if (step.backdrop) {
|
||||
_this._hideBackdrop();
|
||||
}
|
||||
if (step.onHidden != null) {
|
||||
return step.onHidden(_this);
|
||||
}
|
||||
};
|
||||
this._callOnPromiseDone(promise, hideStepHelper);
|
||||
return promise;
|
||||
};
|
||||
|
||||
Tour.prototype.showStep = function(i) {
|
||||
var promise, showStepHelper, skipToPrevious, step,
|
||||
_this = this;
|
||||
step = this.getStep(i);
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
skipToPrevious = i < this._current;
|
||||
promise = this._makePromise(step.onShow != null ? step.onShow(this, i) : void 0);
|
||||
showStepHelper = function(e) {
|
||||
var current_path, path;
|
||||
_this.setCurrentStep(i);
|
||||
path = $.isFunction(step.path) ? step.path.call() : _this._options.basePath + step.path;
|
||||
current_path = [document.location.pathname, document.location.hash].join("");
|
||||
if (_this._isRedirect(path, current_path)) {
|
||||
_this._redirect(step, path);
|
||||
return;
|
||||
}
|
||||
if (_this._isOrphan(step)) {
|
||||
if (!step.orphan) {
|
||||
_this._debug("Skip the orphan step " + (_this._current + 1) + ". Orphan option is false and the element doesn't exist or is hidden.");
|
||||
if (skipToPrevious) {
|
||||
_this._showPrevStep();
|
||||
} else {
|
||||
_this._showNextStep();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_this._debug("Show the orphan step " + (_this._current + 1) + ". Orphans option is true.");
|
||||
}
|
||||
if (step.backdrop) {
|
||||
_this._showBackdrop(!_this._isOrphan(step) ? step.element : void 0);
|
||||
}
|
||||
_this._showPopover(step, i);
|
||||
if (step.onShown != null) {
|
||||
step.onShown(_this);
|
||||
}
|
||||
return _this._debug("Step " + (_this._current + 1) + " of " + _this._steps.length);
|
||||
};
|
||||
return this._callOnPromiseDone(promise, showStepHelper);
|
||||
};
|
||||
|
||||
Tour.prototype.setCurrentStep = function(value) {
|
||||
if (value != null) {
|
||||
this._current = value;
|
||||
return this.setState("current_step", value);
|
||||
} else {
|
||||
this._current = this.getState("current_step");
|
||||
return this._current = this._current === null ? 0 : parseInt(this._current, 10);
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._showNextStep = function() {
|
||||
var promise, showNextStepHelper, step,
|
||||
_this = this;
|
||||
step = this.getStep(this._current);
|
||||
showNextStepHelper = function(e) {
|
||||
return _this.showStep(step.next);
|
||||
};
|
||||
promise = this._makePromise((step.onNext != null ? step.onNext(this) : void 0));
|
||||
return this._callOnPromiseDone(promise, showNextStepHelper);
|
||||
};
|
||||
|
||||
Tour.prototype._showPrevStep = function() {
|
||||
var promise, showPrevStepHelper, step,
|
||||
_this = this;
|
||||
step = this.getStep(this._current);
|
||||
showPrevStepHelper = function(e) {
|
||||
return _this.showStep(step.prev);
|
||||
};
|
||||
promise = this._makePromise((step.onPrev != null ? step.onPrev(this) : void 0));
|
||||
return this._callOnPromiseDone(promise, showPrevStepHelper);
|
||||
};
|
||||
|
||||
Tour.prototype._debug = function(text) {
|
||||
if (this._options.debug) {
|
||||
return window.console.log("Bootstrap Tour '" + this._options.name + "' | " + text);
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._isRedirect = function(path, currentPath) {
|
||||
return (path != null) && path !== "" && path.replace(/\?.*$/, "").replace(/\/?$/, "") !== currentPath.replace(/\/?$/, "");
|
||||
};
|
||||
|
||||
Tour.prototype._redirect = function(step, path) {
|
||||
if ($.isFunction(step.redirect)) {
|
||||
return step.redirect.call(this, path);
|
||||
} else if (step.redirect === true) {
|
||||
this._debug("Redirect to " + path);
|
||||
return document.location.href = path;
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._isOrphan = function(step) {
|
||||
return (step.element == null) || !$(step.element).length || $(step.element).is(":hidden");
|
||||
};
|
||||
|
||||
Tour.prototype._showPopover = function(step, i) {
|
||||
var $element, $navigation, $template, $tip, isOrphan, options,
|
||||
_this = this;
|
||||
options = $.extend({}, this._options);
|
||||
$template = $.isFunction(step.template) ? $(step.template(i, step)) : $(step.template);
|
||||
$navigation = $template.find(".popover-navigation");
|
||||
isOrphan = this._isOrphan(step);
|
||||
if (isOrphan) {
|
||||
step.element = "body";
|
||||
step.placement = "top";
|
||||
$template = $template.addClass("orphan");
|
||||
}
|
||||
$element = $(step.element);
|
||||
$template.addClass("tour-" + this._options.name);
|
||||
if (step.options) {
|
||||
$.extend(options, step.options);
|
||||
}
|
||||
if (step.reflex) {
|
||||
$element.css("cursor", "pointer").on("click.tour-" + this._options.name, function(e) {
|
||||
if (_this._current < _this._steps.length - 1) {
|
||||
return _this.next();
|
||||
} else {
|
||||
return _this.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (step.prev < 0) {
|
||||
$navigation.find("*[data-role=prev]").addClass("disabled");
|
||||
}
|
||||
if (step.next < 0) {
|
||||
$navigation.find("*[data-role=next]").addClass("disabled");
|
||||
}
|
||||
step.template = $template.clone().wrap("<div>").parent().html();
|
||||
$element.popover({
|
||||
placement: step.placement,
|
||||
trigger: "manual",
|
||||
title: step.title,
|
||||
content: step.content,
|
||||
html: true,
|
||||
animation: step.animation,
|
||||
container: step.container,
|
||||
template: step.template,
|
||||
selector: step.element
|
||||
}).popover("show");
|
||||
$tip = $element.data("bs.popover") ? $element.data("bs.popover").tip() : $element.data("popover").tip();
|
||||
$tip.attr("id", step.id);
|
||||
this._scrollIntoView($tip);
|
||||
this._reposition($tip, step);
|
||||
if (isOrphan) {
|
||||
return this._center($tip);
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._reposition = function($tip, step) {
|
||||
var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset;
|
||||
offsetWidth = $tip[0].offsetWidth;
|
||||
offsetHeight = $tip[0].offsetHeight;
|
||||
tipOffset = $tip.offset();
|
||||
originalLeft = tipOffset.left;
|
||||
originalTop = tipOffset.top;
|
||||
offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight();
|
||||
if (offsetBottom < 0) {
|
||||
tipOffset.top = tipOffset.top + offsetBottom;
|
||||
}
|
||||
offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth();
|
||||
if (offsetRight < 0) {
|
||||
tipOffset.left = tipOffset.left + offsetRight;
|
||||
}
|
||||
if (tipOffset.top < 0) {
|
||||
tipOffset.top = 0;
|
||||
}
|
||||
if (tipOffset.left < 0) {
|
||||
tipOffset.left = 0;
|
||||
}
|
||||
$tip.offset(tipOffset);
|
||||
if (step.placement === "bottom" || step.placement === "top") {
|
||||
if (originalLeft !== tipOffset.left) {
|
||||
return this._replaceArrow($tip, (tipOffset.left - originalLeft) * 2, offsetWidth, "left");
|
||||
}
|
||||
} else {
|
||||
if (originalTop !== tipOffset.top) {
|
||||
return this._replaceArrow($tip, (tipOffset.top - originalTop) * 2, offsetHeight, "top");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._center = function($tip) {
|
||||
return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2);
|
||||
};
|
||||
|
||||
Tour.prototype._replaceArrow = function($tip, delta, dimension, position) {
|
||||
return $tip.find(".arrow").css(position, delta ? 50 * (1 - delta / dimension) + "%" : "");
|
||||
};
|
||||
|
||||
Tour.prototype._scrollIntoView = function(tip) {
|
||||
return $("html, body").stop().animate({
|
||||
scrollTop: Math.ceil(tip.offset().top - ($(window).height() / 2))
|
||||
});
|
||||
};
|
||||
|
||||
Tour.prototype._onResize = function(callback, timeout) {
|
||||
return $(window).on("resize.tour-" + this._options.name, function() {
|
||||
clearTimeout(timeout);
|
||||
return timeout = setTimeout(callback, 100);
|
||||
});
|
||||
};
|
||||
|
||||
Tour.prototype._setupKeyboardNavigation = function() {
|
||||
var _this = this;
|
||||
if (this._options.keyboard) {
|
||||
return $(document).on("keyup.tour-" + this._options.name, function(e) {
|
||||
if (!e.which) {
|
||||
return;
|
||||
}
|
||||
switch (e.which) {
|
||||
case 39:
|
||||
e.preventDefault();
|
||||
if (_this._current < _this._steps.length - 1) {
|
||||
return _this.next();
|
||||
} else {
|
||||
return _this.end();
|
||||
}
|
||||
break;
|
||||
case 37:
|
||||
e.preventDefault();
|
||||
if (_this._current > 0) {
|
||||
return _this.prev();
|
||||
}
|
||||
break;
|
||||
case 27:
|
||||
e.preventDefault();
|
||||
return _this.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._makePromise = function(result) {
|
||||
if (result && $.isFunction(result.then)) {
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._callOnPromiseDone = function(promise, cb, arg) {
|
||||
var _this = this;
|
||||
if (promise) {
|
||||
return promise.then(function(e) {
|
||||
return cb.call(_this, arg);
|
||||
});
|
||||
} else {
|
||||
return cb.call(this, arg);
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._showBackdrop = function(element) {
|
||||
if (this.backdrop.overlay !== null) {
|
||||
return;
|
||||
}
|
||||
this._showOverlay();
|
||||
if (element != null) {
|
||||
return this._showOverlayElement(element);
|
||||
}
|
||||
};
|
||||
|
||||
Tour.prototype._hideBackdrop = function() {
|
||||
if (this.backdrop.overlay === null) {
|
||||
return;
|
||||
}
|
||||
if (this.backdrop.$element) {
|
||||
this._hideOverlayElement();
|
||||
}
|
||||
return this._hideOverlay();
|
||||
};
|
||||
|
||||
Tour.prototype._showOverlay = function() {
|
||||
this.backdrop = $("<div/>", {
|
||||
"class": "tour-backdrop"
|
||||
});
|
||||
return $("body").append(this.backdrop);
|
||||
};
|
||||
|
||||
Tour.prototype._hideOverlay = function() {
|
||||
this.backdrop.remove();
|
||||
return this.backdrop.overlay = null;
|
||||
};
|
||||
|
||||
Tour.prototype._showOverlayElement = function(element) {
|
||||
var $background, $element, offset;
|
||||
$element = $(element);
|
||||
$background = $("<div/>");
|
||||
offset = $element.offset();
|
||||
offset.top = offset.top;
|
||||
offset.left = offset.left;
|
||||
$background.width($element.innerWidth()).height($element.innerHeight()).addClass("tour-step-background").offset(offset);
|
||||
$element.addClass("tour-step-backdrop");
|
||||
$("body").append($background);
|
||||
this.backdrop.$element = $element;
|
||||
return this.backdrop.$background = $background;
|
||||
};
|
||||
|
||||
Tour.prototype._hideOverlayElement = function() {
|
||||
this.backdrop.$element.removeClass("tour-step-backdrop");
|
||||
this.backdrop.$background.remove();
|
||||
this.backdrop.$element = null;
|
||||
return this.backdrop.$background = null;
|
||||
};
|
||||
|
||||
return Tour;
|
||||
|
||||
})();
|
||||
return window.Tour = Tour;
|
||||
})(jQuery, window);
|
||||
|
||||
}).call(this);
|
|
@ -518,10 +518,30 @@ div.tour-backdrop {
|
|||
z-index: 2009;
|
||||
}
|
||||
|
||||
.popover.tour {
|
||||
z-index: 2010;
|
||||
.popover.tour.orphan .arrow {
|
||||
display: none;
|
||||
}
|
||||
.popover.tour .popover-navigation {
|
||||
padding: 9px 14px;
|
||||
}
|
||||
.popover.tour .popover-navigation *[data-role="end"] {
|
||||
float: right;
|
||||
}
|
||||
.popover.tour .popover-navigation *[data-role="next"], .popover.tour .popover-navigation *[data-role="end"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popover.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.tour-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1100;
|
||||
background-color: black;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
|
|
@ -451,9 +451,26 @@ $editorbar_height: 30px
|
|||
div.tour-backdrop
|
||||
z-index: 2009
|
||||
.popover.tour
|
||||
z-index: 2010
|
||||
&.orphan .arrow
|
||||
display: none
|
||||
.popover-navigation
|
||||
padding: 9px 14px
|
||||
*[data-role="end"]
|
||||
float: right
|
||||
*[data-role="next"],*[data-role="end"]
|
||||
cursor: pointer
|
||||
.popover.fixed
|
||||
position: fixed
|
||||
.tour-backdrop
|
||||
position: fixed
|
||||
top: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
z-index: 1100
|
||||
background-color: #000
|
||||
opacity: 0.8
|
||||
|
||||
|
||||
// }}}
|
||||
|
||||
|
|
|
@ -545,7 +545,7 @@
|
|||
|
||||
observer.disconnect();
|
||||
var editor = this.rte.editor;
|
||||
var root = editor.element.$;
|
||||
var root = editor.element && editor.element.$;
|
||||
editor.destroy();
|
||||
// FIXME: select editables then filter by dirty?
|
||||
var defs = this.rte.fetch_editables(root)
|
||||
|
|
|
@ -362,7 +362,6 @@
|
|||
},
|
||||
clean_for_save: function () {
|
||||
var self = this;
|
||||
|
||||
$("*[contentEditable], *[attributeEditable]")
|
||||
.removeAttr('contentEditable')
|
||||
.removeAttr('attributeEditable');
|
||||
|
|
|
@ -4,127 +4,116 @@
|
|||
var website = openerp.website;
|
||||
var _t = openerp._t;
|
||||
|
||||
website.EditorBar.include({
|
||||
start: function () {
|
||||
this.registerTour(new website.Tour.Banner(this));
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
|
||||
website.Tour.Banner = website.Tour.extend({
|
||||
website.Tour.register({
|
||||
id: 'banner',
|
||||
name: "Build a page",
|
||||
name: _t("Build a page"),
|
||||
path: '/page/website.homepage',
|
||||
init: function () {
|
||||
var self = this;
|
||||
self.steps = [
|
||||
{
|
||||
title: _t("Welcome to your website!"),
|
||||
content: _t("This tutorial will guide you to build your home page. We will start by adding a banner."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip It") },
|
||||
},
|
||||
{
|
||||
waitNot: '.popover.tour',
|
||||
element: 'button[data-action=edit]',
|
||||
placement: 'bottom',
|
||||
title: _t("Edit this page"),
|
||||
content: _t("Every page of your website can be modified through the <i>Edit</i> button."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Insert building blocks"),
|
||||
content: _t("Click here to insert blocks of content in the page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:first',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a Banner"),
|
||||
content: _t("Drag the Banner block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: '.oe_overlay_options .oe_options:visible',
|
||||
element: '#wrap .carousel:first div.carousel-content',
|
||||
placement: 'top',
|
||||
title: _t("Customize banner's text"),
|
||||
content: _t("Click in the text and start editing it."),
|
||||
sampleText: 'Here, a customized text',
|
||||
},
|
||||
{
|
||||
waitNot: '#wrap .carousel:first div.carousel-content:has(h2:'+
|
||||
'containsExact('+_t('Your Banner Title')+')):has(h3:'+
|
||||
'containsExact('+_t('Click to customize this text')+'))',
|
||||
element: '.oe_snippet_parent:visible',
|
||||
placement: 'bottom',
|
||||
title: _t("Get banner properties"),
|
||||
content: _t("Select the parent container to get the global options of the banner."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.oe_overlay_options .oe_options:visible',
|
||||
placement: 'left',
|
||||
title: _t("Customize the banner"),
|
||||
content: _t("Customize any block through this menu. Try to change the background of the banner."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
waitNot: '.popover.tour',
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Add Another Block"),
|
||||
content: _t("Let's add another building block to your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(6)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop This Block"),
|
||||
content: _t("Drag the <em>'Features'</em> block and drop it below the banner."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: '.oe_overlay_options .oe_options:visible',
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save your modifications"),
|
||||
content: _t("Publish your page by clicking on the <em>'Save'</em> button."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: 'button[data-action=edit]:visible',
|
||||
title: _t("Good Job!"),
|
||||
content: _t("Well done, you created your homepage."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
waitNot: '.popover.tour',
|
||||
element: 'a[data-action=show-mobile-preview]',
|
||||
placement: 'bottom',
|
||||
title: _t("Test Your Mobile Version"),
|
||||
content: _t("Let's check how your homepage looks like on mobile devices."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal:has(#mobile-viewport) button[data-dismiss=modal]',
|
||||
placement: 'right',
|
||||
title: _t("Check Mobile Preview"),
|
||||
content: _t("Scroll to check rendering and then close the mobile preview."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
waitNot: '.modal',
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Add new pages and menus"),
|
||||
content: _t("The 'Content' menu allows you to add pages or add the top menu."),
|
||||
popover: { next: _t("Close Tutorial") },
|
||||
},
|
||||
];
|
||||
return this._super();
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: _t("Welcome to your website!"),
|
||||
content: _t("This tutorial will guide you to build your home page. We will start by adding a banner."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip It") },
|
||||
},
|
||||
{
|
||||
waitNot: '.popover.tour',
|
||||
element: 'button[data-action=edit]',
|
||||
placement: 'bottom',
|
||||
title: _t("Edit this page"),
|
||||
content: _t("Every page of your website can be modified through the <i>Edit</i> button."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Insert building blocks"),
|
||||
content: _t("Click here to insert blocks of content in the page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:first',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a Banner"),
|
||||
content: _t("Drag the Banner block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: '.oe_overlay_options .oe_options:visible',
|
||||
element: '#wrap .carousel:first div.carousel-content',
|
||||
placement: 'top',
|
||||
title: _t("Customize banner's text"),
|
||||
content: _t("Click in the text and start editing it."),
|
||||
sampleText: 'Here, a customized text',
|
||||
},
|
||||
{
|
||||
waitNot: '#wrap .carousel:first div.carousel-content:has(h2:'+
|
||||
'containsExact('+_t('Your Banner Title')+')):has(h3:'+
|
||||
'containsExact('+_t('Click to customize this text')+'))',
|
||||
element: '.oe_snippet_parent:visible',
|
||||
placement: 'bottom',
|
||||
title: _t("Get banner properties"),
|
||||
content: _t("Select the parent container to get the global options of the banner."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.oe_overlay_options .oe_options:visible',
|
||||
placement: 'left',
|
||||
title: _t("Customize the banner"),
|
||||
content: _t("Customize any block through this menu. Try to change the background of the banner."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
waitNot: '.popover.tour',
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Add Another Block"),
|
||||
content: _t("Let's add another building block to your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(6)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop This Block"),
|
||||
content: _t("Drag the <em>'Features'</em> block and drop it below the banner."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: '.oe_overlay_options .oe_options:visible',
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save your modifications"),
|
||||
content: _t("Publish your page by clicking on the <em>'Save'</em> button."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: 'button[data-action=edit]:visible',
|
||||
title: _t("Good Job!"),
|
||||
content: _t("Well done, you created your homepage."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
waitNot: '.popover.tour',
|
||||
element: 'a[data-action=show-mobile-preview]',
|
||||
placement: 'bottom',
|
||||
title: _t("Test Your Mobile Version"),
|
||||
content: _t("Let's check how your homepage looks like on mobile devices."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal:has(#mobile-viewport) button[data-dismiss=modal]',
|
||||
placement: 'right',
|
||||
title: _t("Check Mobile Preview"),
|
||||
content: _t("Scroll to check rendering and then close the mobile preview."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
waitNot: '.modal',
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Add new pages and menus"),
|
||||
content: _t("The 'Content' menu allows you to add pages or add the top menu."),
|
||||
popover: { next: _t("Close Tutorial") },
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
}());
|
||||
|
|
|
@ -6,7 +6,7 @@ if (typeof openerp === "undefined") {
|
|||
var error = "openerp is undefined"
|
||||
+ "\nhref: " + window.location.href
|
||||
+ "\nreferrer: " + document.referrer
|
||||
+ "\nlocalStorage: " + JSON.stringify(window.localStorage);
|
||||
+ "\nlocalStorage: " + window.localStorage.getItem("tour");
|
||||
if (typeof $ !== "undefined") {
|
||||
error += '\n\n' + $("body").html();
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ if (typeof openerp === "undefined") {
|
|||
|
||||
var website = window.openerp.website;
|
||||
|
||||
// don't rewrite website.Tour in test mode
|
||||
// don't rewrite T in test mode
|
||||
if (typeof website.Tour !== "undefined") {
|
||||
return;
|
||||
}
|
||||
|
@ -23,40 +23,26 @@ if (typeof website.Tour !== "undefined") {
|
|||
// don't need template to use bootstrap Tour in automatic mode
|
||||
if (typeof QWeb2 !== "undefined") {
|
||||
website.add_template_file('/website/static/src/xml/website.tour.xml');
|
||||
|
||||
}
|
||||
|
||||
// don't need to use bootstrap Tour to launch an automatic tour
|
||||
function bootstrap_tour_stub () {
|
||||
if (typeof Tour === "undefined") {
|
||||
window.Tour = function Tour() {};
|
||||
Tour.prototype.addSteps = function () {};
|
||||
Tour.prototype.end = function () {};
|
||||
Tour.prototype.goto = function () {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (website.EditorBar) {
|
||||
website.EditorBar.include({
|
||||
tours: [],
|
||||
start: function () {
|
||||
var self = this;
|
||||
var menu = $('#help-menu');
|
||||
_.each(this.tours, function (tour) {
|
||||
_.each(T.tours, function (tour) {
|
||||
if (tour.mode === "test") {
|
||||
return;
|
||||
}
|
||||
var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
|
||||
$menuItem.click(function () {
|
||||
tour.reset();
|
||||
tour.run();
|
||||
T.reset();
|
||||
T.run(tour.id);
|
||||
});
|
||||
menu.append($menuItem);
|
||||
});
|
||||
return this._super();
|
||||
},
|
||||
registerTour: function (tour) {
|
||||
website.Tour.add(tour);
|
||||
this.tours.push(tour);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -94,351 +80,401 @@ $.ajaxSetup({
|
|||
}
|
||||
});
|
||||
|
||||
website.Tour = openerp.Class.extend({
|
||||
steps: [],
|
||||
defaultDelay: 50, //ms
|
||||
defaultOverLaps: 5000, //ms
|
||||
localStorage: window.localStorage,
|
||||
init: function () {},
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
run: function (automatic) {
|
||||
this.reset();
|
||||
var localStorage = window.localStorage;
|
||||
|
||||
for (var k in this.localStorage) {
|
||||
if (!k.indexOf("tour-") && k.indexOf("-test") > -1) return;
|
||||
}
|
||||
|
||||
website.Tour.busy = true;
|
||||
|
||||
if (automatic) {
|
||||
this.localStorage.setItem("tour-"+this.id+"-test-automatic", true);
|
||||
var T = website.Tour = {
|
||||
tours: {},
|
||||
defaultDelay: 50,
|
||||
retryRunningDelay: 1000,
|
||||
errorDelay: 5000,
|
||||
state: null,
|
||||
$element: null,
|
||||
timer: null,
|
||||
testtimer: null,
|
||||
currentTimer: null,
|
||||
register: function (tour) {
|
||||
if (tour.mode !== "test") tour.mode = "tutorial";
|
||||
T.tours[tour.id] = tour;
|
||||
},
|
||||
run: function (tour_id, mode) {
|
||||
var tour = T.tours[tour_id];
|
||||
this.time = new Date().getTime();
|
||||
if (tour.path && !window.location.href.match(new RegExp("("+T.getLang()+")?"+tour.path+"#?$", "i"))) {
|
||||
var href = "/"+T.getLang()+tour.path;
|
||||
console.log("Tour Begin from run method (redirection to "+href+")");
|
||||
T.saveState(tour.id, mode || tour.mode, -1);
|
||||
window.location.href = href;
|
||||
} else {
|
||||
this.localStorage.removeItem("tour-"+this.id+"-test-automatic");
|
||||
console.log("Tour Begin from run method");
|
||||
T.saveState(tour.id, mode || tour.mode, 0);
|
||||
T.running();
|
||||
}
|
||||
this.automatic = automatic;
|
||||
|
||||
if (this.path) {
|
||||
// redirect to begin of the tour in function of the language
|
||||
if (!this.testUrl(this.path+"(#.*)?$")) {
|
||||
var path = this.path.split('#');
|
||||
window.location.href = "/"+this.getLang()+path[0] + "#tutorial."+this.id+"=true&" + path.slice(1, path.length).join("#");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.localStorage.setItem("tour-"+this.id+"-test", 0);
|
||||
website.Tour.waitReady.call(this, function () {self._running();});
|
||||
},
|
||||
running: function () {
|
||||
var self = this;
|
||||
if (+this.localStorage.getItem("tour-"+this.id+"-test") >= this.steps.length-1) {
|
||||
this.endTour();
|
||||
registerSteps: function (tour) {
|
||||
if (tour.register) {
|
||||
return;
|
||||
}
|
||||
tour.register = true;
|
||||
|
||||
if (website.Tour.is_busy()) return;
|
||||
for (var index=0, len=tour.steps.length; index<len; index++) {
|
||||
var step = tour.steps[index];
|
||||
step.id = index;
|
||||
|
||||
// launch tour with url
|
||||
this.checkRunningUrl();
|
||||
|
||||
// mark tour as busy (only one test running)
|
||||
if (this.localStorage.getItem("tour-"+this.id+"-test") != null) {
|
||||
website.Tour.busy = true;
|
||||
this.automatic = !!this.localStorage.getItem("tour-"+this.id+"-test-automatic");
|
||||
}
|
||||
|
||||
if (!this.testPathUrl()) {
|
||||
if (this.automatic) {
|
||||
this.timer = setTimeout(function () {
|
||||
self.reset();
|
||||
throw new Error("Wrong url for running " + self.id
|
||||
+ '\ntestPath: ' + self.testPath
|
||||
+ '\nhref: ' + window.location.href
|
||||
+ "\nreferrer: " + document.referrer
|
||||
);
|
||||
},this.defaultOverLaps);
|
||||
if (!step.waitNot && index > 0 && tour.steps[index-1] &&
|
||||
tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
|
||||
step.waitNot = '.popover.tour.fade.in:visible';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
website.Tour.waitReady.call(this, function () {self._running();});
|
||||
},
|
||||
_running: function () {
|
||||
var stepId = this.localStorage.getItem("tour-"+this.id+"-test");
|
||||
|
||||
if (stepId != null) {
|
||||
this.registerTour();
|
||||
this.nextStep(stepId, this.automatic ? this.autoNextStep : null, this.automatic ? this.defaultOverLaps : null);
|
||||
}
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
website.Tour.busy = false;
|
||||
for (var k in this.steps) {
|
||||
this.steps[k].busy = false;
|
||||
}
|
||||
clearTimeout(self.timer);
|
||||
clearTimeout(self.testtimer);
|
||||
|
||||
for (var k in this.localStorage) {
|
||||
if (!k.indexOf("tour-") || !k.indexOf(this.id)) {
|
||||
this.localStorage.removeItem(k);
|
||||
}
|
||||
}
|
||||
|
||||
$('.popover.tour').remove();
|
||||
},
|
||||
|
||||
getLang: function () {
|
||||
return $("html").attr("lang").replace(/-/, '_');
|
||||
},
|
||||
testUrl: function (url) {
|
||||
return new RegExp("(/"+this.getLang()+")?"+url, "i").test(window.location.href);
|
||||
},
|
||||
testPathUrl: function () {
|
||||
if (!this.testPath || this.testUrl(this.testPath)) return true;
|
||||
},
|
||||
checkRunningUrl: function () {
|
||||
if (window.location.hash.indexOf("tutorial."+this.id+"=true") > -1) {
|
||||
this.localStorage.setItem("tour-"+this.id+"-test", 0);
|
||||
window.location.hash = window.location.hash.replace(/tutorial.+=true&?/, '');
|
||||
}
|
||||
},
|
||||
|
||||
registerTour: function () {
|
||||
if (this.automatic) {
|
||||
bootstrap_tour_stub();
|
||||
}
|
||||
this.tour = new Tour({
|
||||
name: this.id,
|
||||
storage: this.tourStorage,
|
||||
keyboard: false,
|
||||
template: this.popover(),
|
||||
onHide: function () {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
});
|
||||
this.registerSteps();
|
||||
},
|
||||
registerSteps: function () {
|
||||
for (var index=0, len=this.steps.length; index<len; index++) {
|
||||
var step = this.steps[index];
|
||||
step.stepId = step.stepId || ""+index;
|
||||
|
||||
if (!step.waitNot && index > 0 && this.steps[index-1] &&
|
||||
this.steps[index-1].popover && this.steps[index-1].popover.next) {
|
||||
step.waitNot = '.popover.tour:visible';
|
||||
}
|
||||
if (!step.waitFor && index > 0 && this.steps[index-1].snippet) {
|
||||
if (!step.waitFor && index > 0 && tour.steps[index-1].snippet) {
|
||||
step.waitFor = '.oe_overlay_options .oe_options:visible';
|
||||
}
|
||||
|
||||
step._title = step._title || step.title;
|
||||
step.title = this.popoverTitle({ title: step._title });
|
||||
step.template = step.template || this.popover( step.popover );
|
||||
|
||||
if (!step.element) step.orphan = true;
|
||||
if (step.snippet) {
|
||||
var snippet = step.element && step.element.match(/#oe_snippets (.*) \.oe_snippet_thumbnail/);
|
||||
if (snippet) {
|
||||
step.snippet = snippet[1];
|
||||
} else if (step.snippet) {
|
||||
step.element = '#oe_snippets '+step.snippet+' .oe_snippet_thumbnail';
|
||||
}
|
||||
|
||||
if (!step.element) {
|
||||
step.element = "body";
|
||||
step.orphan = true;
|
||||
step.backdrop = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.steps[index-1] &&
|
||||
this.steps[index-1].popover && this.steps[index-1].popover.next) {
|
||||
if (tour.steps[index-1] &&
|
||||
tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
|
||||
var step = {
|
||||
stepId: ""+index,
|
||||
waitNot: '.popover.tour:visible'
|
||||
_title: "",
|
||||
id: index,
|
||||
waitNot: '.popover.tour.fade.in:visible'
|
||||
};
|
||||
this.steps.push(step);
|
||||
tour.steps.push(step);
|
||||
}
|
||||
|
||||
this.tour.addSteps(this.steps);
|
||||
// rendering bootstrap tour and popover
|
||||
if (tour.mode !== "test") {
|
||||
for (var index=0, len=tour.steps.length; index<len; index++) {
|
||||
var step = tour.steps[index];
|
||||
step._title = step._title || step.title;
|
||||
step.title = T.popoverTitle(tour, { title: step._title });
|
||||
step.template = step.template || T.popover( step.popover );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
popoverTitle: function (options) {
|
||||
try {
|
||||
return openerp.qweb.render('website.tour_popover_title', options);
|
||||
} catch (e) {
|
||||
if (!this.automatic) throw e;
|
||||
return options.title;
|
||||
closePopover: function () {
|
||||
if (T.$element) {
|
||||
T.$element.popover('destroy');
|
||||
T.$element.removeData("tour");
|
||||
T.$element.removeData("tour-step");
|
||||
$(".tour-backdrop").remove();
|
||||
$(".popover.tour").remove();
|
||||
T.$element = null;
|
||||
}
|
||||
},
|
||||
autoTogglePopover: function () {
|
||||
var state = T.getState();
|
||||
var step = state.step;
|
||||
|
||||
if (T.$element &&
|
||||
T.$element.is(":visible") &&
|
||||
T.$element.data("tour") === state.id &&
|
||||
T.$element.data("tour-step") === step.id) {
|
||||
T.repositionPopover();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
T.closePopover();
|
||||
|
||||
var $element = $(step.element).first();
|
||||
if (!step.element || !$element.size() || !$element.is(":visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
T.$element = $element;
|
||||
$element.data("tour", state.id);
|
||||
$element.data("tour-step", step.id);
|
||||
$element.popover({
|
||||
placement: step.placement || "auto",
|
||||
animation: true,
|
||||
trigger: "manual",
|
||||
title: step.title,
|
||||
content: step.content,
|
||||
html: true,
|
||||
container: "body",
|
||||
template: step.template,
|
||||
orphan: step.orphan
|
||||
}).popover("show");
|
||||
|
||||
|
||||
var $tip = $element.data("bs.popover").tip();
|
||||
|
||||
|
||||
// add popover style (orphan, static, backdrop)
|
||||
if (step.orphan) {
|
||||
$tip.addClass("orphan");
|
||||
}
|
||||
|
||||
var node = $element[0];
|
||||
var css;
|
||||
do {
|
||||
css = window.getComputedStyle(node);
|
||||
if (!css || css.position == "fixed") {
|
||||
$tip.addClass("fixed");
|
||||
break;
|
||||
}
|
||||
} while ((node = node.parentNode) && node !== document);
|
||||
|
||||
if (step.backdrop) {
|
||||
$("body").append('<div class="tour-backdrop"></div>');
|
||||
}
|
||||
|
||||
if (step.backdrop || $element.parents("#website-top-navbar, .modal").size()) {
|
||||
$tip.css("z-index", 2010);
|
||||
}
|
||||
|
||||
// button click event
|
||||
$tip.find("button")
|
||||
.one("click", function () {
|
||||
step.busy = true;
|
||||
if (!$(this).is("[data-role='next']")) {
|
||||
clearTimeout(T.timer);
|
||||
T.endTour();
|
||||
}
|
||||
T.closePopover();
|
||||
});
|
||||
|
||||
T.repositionPopover();
|
||||
},
|
||||
repositionPopover: function() {
|
||||
var popover = T.$element.data("bs.popover");
|
||||
var $tip = T.$element.data("bs.popover").tip();
|
||||
|
||||
if (popover.options.orphan) {
|
||||
return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2);
|
||||
}
|
||||
|
||||
var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset;
|
||||
offsetWidth = $tip[0].offsetWidth;
|
||||
offsetHeight = $tip[0].offsetHeight;
|
||||
tipOffset = $tip.offset();
|
||||
originalLeft = tipOffset.left;
|
||||
originalTop = tipOffset.top;
|
||||
offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight();
|
||||
if (offsetBottom < 0) {
|
||||
tipOffset.top = tipOffset.top + offsetBottom;
|
||||
}
|
||||
offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth();
|
||||
if (offsetRight < 0) {
|
||||
tipOffset.left = tipOffset.left + offsetRight;
|
||||
}
|
||||
if (tipOffset.top < 0) {
|
||||
tipOffset.top = 0;
|
||||
}
|
||||
if (tipOffset.left < 0) {
|
||||
tipOffset.left = 0;
|
||||
}
|
||||
$tip.offset(tipOffset);
|
||||
if (popover.options.placement === "bottom" || popover.options.placement === "top") {
|
||||
var left = T.$element.offset().left + T.$element.outerWidth()/2 - tipOffset.left;
|
||||
$tip.find(".arrow").css("left", left ? left + "px" : "");
|
||||
} else if (popover.options.placement !== "auto") {
|
||||
var top = T.$element.offset().top + T.$element.outerHeight()/2 - tipOffset.top;
|
||||
$tip.find(".arrow").css("top", top ? top + "px" : "");
|
||||
}
|
||||
},
|
||||
popoverTitle: function (tour, options) {
|
||||
return openerp.qweb ? openerp.qweb.render('website.tour_popover_title', options) : options.title;
|
||||
},
|
||||
popover: function (options) {
|
||||
try {
|
||||
return openerp.qweb.render('website.tour_popover', options);
|
||||
} catch (e) {
|
||||
if (!this.automatic) throw e;
|
||||
return "";
|
||||
}
|
||||
return openerp.qweb ? openerp.qweb.render('website.tour_popover', options) : options.title;
|
||||
},
|
||||
getLang: function () {
|
||||
return $("html").attr("lang").replace(/-/, '_');
|
||||
},
|
||||
getState: function () {
|
||||
var state = JSON.parse(localStorage.getItem("tour") || 'false') || {};
|
||||
if (state) { this.time = state.time; }
|
||||
var tour_id,mode,step_id;
|
||||
if (!state.id && window.location.href.indexOf("#tutorial.") > -1) {
|
||||
state = {
|
||||
"id": window.location.href.match(/#tutorial\.(.*)=true/)[1],
|
||||
"mode": "tutorial",
|
||||
"step_id": 0
|
||||
};
|
||||
window.location.hash = "";
|
||||
console.log("Tour Begin from url hash");
|
||||
T.saveState(state.id, state.mode, state.step_id);
|
||||
}
|
||||
if (!state.id) {
|
||||
return;
|
||||
}
|
||||
state.tour = T.tours[state.id];
|
||||
state.step = state.tour && state.tour.steps[state.step_id === -1 ? 0 : state.step_id];
|
||||
return state;
|
||||
},
|
||||
error: function (step, message) {
|
||||
var state = T.getState();
|
||||
message += '\n tour: ' + state.id
|
||||
+ '\n step: ' + step.id + ": '" + (step._title || step.title) + "'"
|
||||
+ '\n href: ' + window.location.href
|
||||
+ '\n referrer: ' + document.referrer
|
||||
+ '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden")))
|
||||
+ '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size())
|
||||
+ '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size())
|
||||
+ "\n localStorage: " + JSON.stringify(localStorage)
|
||||
+ '\n\n' + $("body").html();
|
||||
T.reset();
|
||||
throw new Error(message);
|
||||
},
|
||||
lists: function () {
|
||||
var tour_ids = [];
|
||||
for (var k in T.tours) {
|
||||
tour_ids.push(k);
|
||||
}
|
||||
return tour_ids;
|
||||
},
|
||||
saveState: function (tour_id, mode, step_id) {
|
||||
localStorage.setItem("tour", JSON.stringify({"id":tour_id, "mode":mode, "step_id":step_id || 0, "time": this.time}));
|
||||
},
|
||||
reset: function () {
|
||||
var state = T.getState();
|
||||
if (state) {
|
||||
for (var k in state.tour.steps) {
|
||||
state.tour.steps[k].busy = false;
|
||||
}
|
||||
}
|
||||
localStorage.removeItem("tour");
|
||||
clearTimeout(T.timer);
|
||||
clearTimeout(T.testtimer);
|
||||
T.closePopover();
|
||||
},
|
||||
running: function () {
|
||||
function run () {
|
||||
var state = T.getState();
|
||||
if (!state) return;
|
||||
if (state.tour) {
|
||||
console.log("Tour '"+state.id+"' is running");
|
||||
T.registerSteps(state.tour);
|
||||
T.nextStep();
|
||||
} else {
|
||||
console.log("Tour '"+state.id+"' wait for running (tour undefined)");
|
||||
setTimeout(T.running, state.mode === "test" ? T.defaultDelay : T.retryRunningDelay);
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
if ($.ajaxBusy) {
|
||||
$(document).ajaxStop(run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
},0);
|
||||
},
|
||||
|
||||
timer: null,
|
||||
testtimer: null,
|
||||
check: function (step) {
|
||||
return (step &&
|
||||
(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) &&
|
||||
(!step.waitNot || !$(step.waitNot).size()) &&
|
||||
(!step.waitFor || $(step.waitFor).size()));
|
||||
},
|
||||
waitNextStep: function (step, callback, overlaps) {
|
||||
var self = this;
|
||||
waitNextStep: function () {
|
||||
var state = T.getState();
|
||||
var time = new Date().getTime();
|
||||
var timer;
|
||||
var next = state.tour.steps[state.step.id+1];
|
||||
var overlaps = state.mode === "test" ? T.errorDelay : 0;
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
clearTimeout(self.timer);
|
||||
clearTimeout(self.testtimer);
|
||||
clearTimeout(T.timer);
|
||||
clearTimeout(T.testtimer);
|
||||
};
|
||||
|
||||
// check popover activity
|
||||
$(".popover.tour button")
|
||||
.off()
|
||||
.on("click", function () {
|
||||
var help = $("#help-menu-button");
|
||||
var offset = help.offset();
|
||||
var left = (offset.left > 0) ? (offset.left + help.width()) : offset.left;
|
||||
var top = (help.height() > 0) ? (offset.top + help.height()) : offset.top;
|
||||
|
||||
if ($(this).is("[data-role='next']") && step.element) {
|
||||
$(".popover.tour").remove();
|
||||
}
|
||||
if (step.busy) return;
|
||||
if (!$(this).is("[data-role='next']") || !step.element) {
|
||||
$('.popover.tour')
|
||||
.animate({
|
||||
left: left,
|
||||
top: top,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
opacity: 0
|
||||
}, 800,
|
||||
function(){
|
||||
$(".popover.tour").remove();
|
||||
clearTimeout(self.timer);
|
||||
step.busy = true;
|
||||
self.tour.end();
|
||||
self.endTour(callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function checkNext () {
|
||||
clearTimeout(self.timer);
|
||||
if (step.busy) return;
|
||||
if (self.check(step)) {
|
||||
step.busy = true;
|
||||
T.autoTogglePopover();
|
||||
|
||||
clearTimeout(T.timer);
|
||||
if (T.check(next)) {
|
||||
clearTimeout(T.currentTimer);
|
||||
// use an other timeout for cke dom loading
|
||||
setTimeout(function () {
|
||||
self.nextStep(step.stepId, callback, overlaps);
|
||||
}, self.defaultDelay);
|
||||
T.nextStep(next);
|
||||
}, T.defaultDelay);
|
||||
} else if (!overlaps || new Date().getTime() - time < overlaps) {
|
||||
if (self.current.element) {
|
||||
var $popover = $(".popover.tour");
|
||||
if(!$(self.current.element).is(":visible")) {
|
||||
$popover.data("hide", true).fadeOut(300);
|
||||
} else if($popover.data("hide")) {
|
||||
$popover.data("hide", false).fadeIn(150);
|
||||
}
|
||||
}
|
||||
self.timer = setTimeout(checkNext, self.defaultDelay);
|
||||
T.timer = setTimeout(checkNext, T.defaultDelay);
|
||||
} else {
|
||||
self.reset();
|
||||
throw new Error("Can't arrive to step " + step.stepId + ": '" + step._title + "'"
|
||||
+ '\nhref: ' + window.location.href
|
||||
+ '\nelement: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden")))
|
||||
+ '\nwaitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size())
|
||||
+ '\nwaitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size())
|
||||
+ '\n\n' + $("body").html()
|
||||
);
|
||||
T.error(next, "Can't reach the next step");
|
||||
}
|
||||
}
|
||||
checkNext();
|
||||
},
|
||||
step: function (stepId) {
|
||||
var steps = this.steps.slice(0,this.steps.length),
|
||||
step;
|
||||
while (step = steps.shift()) {
|
||||
if (!stepId || step.stepId === stepId)
|
||||
return step;
|
||||
nextStep: function (step) {
|
||||
var state = T.getState();
|
||||
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
next: function (stepId) {
|
||||
var steps = this.steps.slice(0,this.steps.length),
|
||||
step, next, index=0;
|
||||
while (step = steps.shift()) {
|
||||
if (!stepId || step.stepId === stepId) {
|
||||
// clear popover (fix for boostrap tour if the element is removed before destroy popover)
|
||||
$(".popover.tour").remove();
|
||||
// go to step in bootstrap tour
|
||||
this.tour.goto(index);
|
||||
if (step.onload) step.onload();
|
||||
next = steps.shift();
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
|
||||
step = step || state.step;
|
||||
T.saveState(state.id, state.mode, step.id);
|
||||
|
||||
if (step.id !== state.step_id) {
|
||||
console.log("Tour Step: '" + (step._title || step.title) + "' (" + (new Date().getTime() - this.time) + "ms)");
|
||||
}
|
||||
return next;
|
||||
},
|
||||
nextStep: function (stepId, callback, overlaps) {
|
||||
var self = this;
|
||||
if (!this.localStorage.getItem("tour-"+this.id+"-test")) return;
|
||||
|
||||
this.localStorage.setItem("tour-"+this.id+"-test", stepId || 0);
|
||||
T.autoTogglePopover(true);
|
||||
|
||||
this.current = this.step(stepId);
|
||||
var next = this.next(stepId);
|
||||
if (step.onload) {
|
||||
step.onload();
|
||||
}
|
||||
|
||||
var next = state.tour.steps[step.id+1];
|
||||
if (next) {
|
||||
setTimeout(function () {
|
||||
self.waitNextStep(next, callback, overlaps);
|
||||
if (callback) setTimeout(function(){callback.call(self, next);}, self.defaultDelay);
|
||||
}, next && next.wait || 0);
|
||||
T.waitNextStep();
|
||||
if (state.mode === "test") {
|
||||
setTimeout(function(){
|
||||
T.autoNextStep(state.tour, step);
|
||||
}, T.defaultDelay);
|
||||
}
|
||||
}, next.wait || 0);
|
||||
} else {
|
||||
this.endTour();
|
||||
T.endTour();
|
||||
}
|
||||
},
|
||||
endTour: function () {
|
||||
var test = parseInt(this.localStorage.getItem("tour-"+this.id+"-test"),10) >= this.steps.length-1;
|
||||
this.reset();
|
||||
var state = T.getState();
|
||||
var test = state.step.id >= state.tour.steps.length-1;
|
||||
T.reset();
|
||||
if (test) {
|
||||
console.log('ok');
|
||||
} else {
|
||||
console.log('error');
|
||||
}
|
||||
},
|
||||
autoNextStep: function () {
|
||||
var self = this;
|
||||
clearTimeout(self.testtimer);
|
||||
autoNextStep: function (tour, step) {
|
||||
clearTimeout(T.testtimer);
|
||||
|
||||
function autoStep () {
|
||||
var step = self.current;
|
||||
if (!step) return;
|
||||
|
||||
if (step.autoComplete) {
|
||||
step.autoComplete(tour);
|
||||
}
|
||||
|
||||
var $popover = $(".popover.tour");
|
||||
if ($popover.find("button[data-role='next']:visible").size()) {
|
||||
$popover.find("button[data-role='next']:visible").click();
|
||||
$popover.remove();
|
||||
}
|
||||
$(".popover.tour [data-role='next']").click();
|
||||
|
||||
var $element = $(step.element);
|
||||
if (!$element.size()) return;
|
||||
|
||||
if (step.snippet) {
|
||||
|
||||
var selector = '#oe_snippets '+step.snippet+' .oe_snippet_thumbnail';
|
||||
self.autoDragAndDropSnippet(selector);
|
||||
|
||||
} else if (step.element.match(/#oe_snippets .* \.oe_snippet_thumbnail/)) {
|
||||
|
||||
self.autoDragAndDropSnippet($element);
|
||||
T.autoDragAndDropSnippet($element);
|
||||
|
||||
} else if ($element.is(":visible")) {
|
||||
|
||||
|
@ -473,7 +509,7 @@ website.Tour = openerp.Class.extend({
|
|||
|
||||
}
|
||||
}
|
||||
self.testtimer = setTimeout(autoStep, 100);
|
||||
T.testtimer = setTimeout(autoStep, 100);
|
||||
},
|
||||
autoDragAndDropSnippet: function (selector) {
|
||||
var $thumbnail = $(selector).first();
|
||||
|
@ -483,64 +519,11 @@ website.Tour = openerp.Class.extend({
|
|||
var $dropZone = $(".oe_drop_zone").first();
|
||||
var dropPosition = $dropZone.position();
|
||||
$dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top }));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
website.Tour.tours = {};
|
||||
website.Tour.busy = false;
|
||||
website.Tour.add = function (tour) {
|
||||
website.Tour.waitReady(function () {
|
||||
tour = tour.id ? tour : new tour();
|
||||
if (!website.Tour.tours[tour.id]) {
|
||||
website.Tour.tours[tour.id] = tour;
|
||||
tour.running();
|
||||
}
|
||||
});
|
||||
};
|
||||
website.Tour.get = function (id) {
|
||||
return website.Tour.tours[id];
|
||||
};
|
||||
website.Tour.each = function (callback) {
|
||||
website.Tour.waitReady(function () {
|
||||
for (var k in website.Tour.tours) {
|
||||
callback.call(website.Tour.tours[k]);
|
||||
}
|
||||
});
|
||||
};
|
||||
website.Tour.waitReady = function (callback) {
|
||||
var self = this;
|
||||
$(document).ready(function () {
|
||||
if ($.ajaxBusy) {
|
||||
$(document).ajaxStop(function() {
|
||||
setTimeout(function () {
|
||||
callback.call(self);
|
||||
},0);
|
||||
});
|
||||
}
|
||||
else {
|
||||
setTimeout(function () {
|
||||
callback.call(self);
|
||||
},0);
|
||||
}
|
||||
});
|
||||
};
|
||||
website.Tour.run_test = function (id) {
|
||||
website.Tour.waitReady(function () {
|
||||
if (!website.Tour.is_busy()) {
|
||||
website.Tour.tours[id].run(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
website.Tour.is_busy = function () {
|
||||
for (var k in this.localStorage) {
|
||||
if (!k.indexOf("tour-")) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return website.Tour.busy;
|
||||
};
|
||||
|
||||
//$(document).ready(T.running);
|
||||
website.ready().then(T.running);
|
||||
|
||||
|
||||
}());
|
||||
|
|
|
@ -8,6 +8,6 @@ class TestUi(openerp.tests.HttpCase):
|
|||
self.phantom_js("/", "console.log('ok')", "openerp.website.editor", login='admin')
|
||||
|
||||
def test_04_admin_tour_banner(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('banner')", "openerp.website.Tour.tours.banner", login='admin')
|
||||
self.phantom_js("/", "openerp.website.Tour.run('banner', 'test')", "openerp.website.Tour.tours.banner", login='admin')
|
||||
|
||||
# vim:et:
|
||||
|
|
|
@ -256,7 +256,6 @@
|
|||
<xpath expr='//script[@src="/web/static/lib/bootstrap/js/bootstrap.js"]' position="before">
|
||||
<link rel='stylesheet' href='/website/static/src/css/snippets.css'/>
|
||||
<link rel='stylesheet' href='/website/static/src/css/editor.css'/>
|
||||
<link rel='stylesheet' href='/website/static/lib/bootstrap-tour/bootstrap-tour.css'/>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/web/static/lib/select2/select2.css"/>
|
||||
|
@ -268,7 +267,6 @@
|
|||
<script t-if="not translatable" type="text/javascript" src="/website/static/lib/ace/ace.js"></script>
|
||||
<script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js"></script>
|
||||
<script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script>
|
||||
<!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) -->
|
||||
<script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script>
|
||||
<script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script>
|
||||
|
|
|
@ -4,117 +4,105 @@
|
|||
var website = openerp.website;
|
||||
var _t = openerp._t;
|
||||
|
||||
website.EditorBar.include({
|
||||
start: function () {
|
||||
this.registerTour(new website.Tour.Blog(this));
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
|
||||
website.Tour.Blog = website.Tour.extend({
|
||||
id: 'blog',
|
||||
name: "Create a blog post",
|
||||
testPath: '/(blog|blogpost)',
|
||||
init: function () {
|
||||
var self = this;
|
||||
self.steps = [
|
||||
{
|
||||
title: _t("New Blog Post"),
|
||||
content: _t("Let's go through the first steps to write beautiful blog posts."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip") },
|
||||
},
|
||||
{
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Add Content"),
|
||||
content: _t("Use this <em>'Content'</em> menu to create a new blog post like any other document (page, menu, products, event, ...)."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'a[data-action=new_blog_post]',
|
||||
placement: 'left',
|
||||
title: _t("New Blog Post"),
|
||||
content: _t("Select this menu item to create a new blog post."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal:has(#editor_new_blog) button.btn-primary',
|
||||
placement: 'right',
|
||||
title: _t("Create Blog Post"),
|
||||
content: _t("Click <em>Continue</em> to create the blog post."),
|
||||
},
|
||||
{
|
||||
waitFor: 'body:has(button[data-action=save]:visible):has(.js_blog)',
|
||||
title: _t("Blog Post Created"),
|
||||
content: _t("This is your new blog post. Let's edit it."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
element: 'h1[data-oe-expression="blog_post.name"]',
|
||||
placement: 'bottom',
|
||||
sampleText: 'New Blog',
|
||||
title: _t("Set a Title"),
|
||||
content: _t("Click on this area and set a catchy title for your blog post."),
|
||||
},
|
||||
{
|
||||
waitNot: '#wrap h1[data-oe-model="blog.post"]:contains("Blog Post Title")',
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'left',
|
||||
title: _t("Layout Your Blog Post"),
|
||||
content: _t("Use well designed building blocks to structure the content of your blog. Click 'Insert Blocks' to add new content."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(2)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a Block"),
|
||||
content: _t("Drag this block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Add Another Block"),
|
||||
content: _t("Let's add another block to your post."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(4)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag this block and drop it below the image block."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.oe_active .oe_snippet_remove',
|
||||
placement: 'top',
|
||||
title: _t("Delete the block"),
|
||||
content: _t("From this toolbar you can move, duplicate or delete the selected zone. Click on the garbage can image to delete the block. Or click on the Title and delete it."),
|
||||
},
|
||||
{
|
||||
waitNot: '.oe_active .oe_snippet_remove:visible',
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save Your Blog"),
|
||||
content: _t("Click the <em>Save</em> button to record changes on the page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: 'button[data-action=edit]:visible',
|
||||
element: 'button.btn-danger.js_publish_btn',
|
||||
placement: 'top',
|
||||
title: _t("Publish Your Post"),
|
||||
content: _t("Your blog post is not yet published. You can update this draft version and publish it once you are ready."),
|
||||
},
|
||||
{
|
||||
waitFor: '.js_publish_management button.js_publish_btn.btn-success:visible',
|
||||
title: "Thanks!",
|
||||
content: _t("This tutorial is finished. To discover more features, improve the content of this page and try the <em>Promote</em> button in the top right menu."),
|
||||
popover: { next: _t("Close Tutorial") },
|
||||
},
|
||||
];
|
||||
return this._super();
|
||||
},
|
||||
website.Tour.register({
|
||||
id: 'blog',
|
||||
name: _t("Create a blog post"),
|
||||
steps: [
|
||||
{
|
||||
title: _t("New Blog Post"),
|
||||
content: _t("Let's go through the first steps to write beautiful blog posts."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip") },
|
||||
},
|
||||
{
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Add Content"),
|
||||
content: _t("Use this <em>'Content'</em> menu to create a new blog post like any other document (page, menu, products, event, ...)."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'a[data-action=new_blog_post]',
|
||||
placement: 'left',
|
||||
title: _t("New Blog Post"),
|
||||
content: _t("Select this menu item to create a new blog post."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal:has(#editor_new_blog) button.btn-primary',
|
||||
placement: 'right',
|
||||
title: _t("Create Blog Post"),
|
||||
content: _t("Click <em>Continue</em> to create the blog post."),
|
||||
},
|
||||
{
|
||||
waitFor: 'body:has(button[data-action=save]:visible):has(.js_blog)',
|
||||
title: _t("Blog Post Created"),
|
||||
content: _t("This is your new blog post. Let's edit it."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
element: 'h1[data-oe-expression="blog_post.name"]',
|
||||
placement: 'bottom',
|
||||
sampleText: 'New Blog',
|
||||
title: _t("Set a Title"),
|
||||
content: _t("Click on this area and set a catchy title for your blog post."),
|
||||
},
|
||||
{
|
||||
waitNot: '#wrap h1[data-oe-model="blog.post"]:contains("Blog Post Title")',
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'left',
|
||||
title: _t("Layout Your Blog Post"),
|
||||
content: _t("Use well designed building blocks to structure the content of your blog. Click 'Insert Blocks' to add new content."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(2)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a Block"),
|
||||
content: _t("Drag this block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Add Another Block"),
|
||||
content: _t("Let's add another block to your post."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(4)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag this block and drop it below the image block."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.oe_active .oe_snippet_remove',
|
||||
placement: 'top',
|
||||
title: _t("Delete the block"),
|
||||
content: _t("From this toolbar you can move, duplicate or delete the selected zone. Click on the garbage can image to delete the block. Or click on the Title and delete it."),
|
||||
},
|
||||
{
|
||||
waitNot: '.oe_active .oe_snippet_remove:visible',
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save Your Blog"),
|
||||
content: _t("Click the <em>Save</em> button to record changes on the page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: 'button[data-action=edit]:visible',
|
||||
element: 'button.btn-danger.js_publish_btn',
|
||||
placement: 'top',
|
||||
title: _t("Publish Your Post"),
|
||||
content: _t("Your blog post is not yet published. You can update this draft version and publish it once you are ready."),
|
||||
},
|
||||
{
|
||||
waitFor: '.js_publish_management button.js_publish_btn.btn-success:visible',
|
||||
title: "Thanks!",
|
||||
content: _t("This tutorial is finished. To discover more features, improve the content of this page and try the <em>Promote</em> button in the top right menu."),
|
||||
popover: { next: _t("Close Tutorial") },
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
}());
|
||||
|
|
|
@ -2,5 +2,5 @@ import openerp.tests
|
|||
|
||||
class TestUi(openerp.tests.HttpCase):
|
||||
def test_admin(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('blog')", "openerp.website.Tour")
|
||||
self.phantom_js("/", "openerp.website.Tour.run('blog', 'test')", "openerp.website.Tour.tours.blog")
|
||||
|
||||
|
|
|
@ -4,115 +4,103 @@
|
|||
var website = openerp.website;
|
||||
var _t = openerp._t;
|
||||
|
||||
website.EditorBar.include({
|
||||
start: function () {
|
||||
this.registerTour(new website.EventTour(this));
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
website.Tour.register({
|
||||
id: 'event',
|
||||
name: _t("Create an event"),
|
||||
steps: [
|
||||
{
|
||||
title: _t("Create an Event"),
|
||||
content: _t("Let's go through the first steps to publish a new event."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip It") },
|
||||
},
|
||||
{
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Add Content"),
|
||||
content: _t("The <em>Content</em> menu allows you to create new pages, events, menus, etc."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'a[data-action=new_event]',
|
||||
placement: 'left',
|
||||
title: _t("New Event"),
|
||||
content: _t("Click here to create a new event."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal #editor_new_event input[type=text]',
|
||||
sampleText: 'Advanced Technical Training',
|
||||
placement: 'right',
|
||||
title: _t("Create an Event Name"),
|
||||
content: _t("Create a name for your new event and click <em>'Continue'</em>. e.g: Technical Training"),
|
||||
},
|
||||
{
|
||||
waitNot: '.modal input[type=text]:not([value!=""])',
|
||||
element: '.modal button.btn-primary',
|
||||
placement: 'right',
|
||||
title: _t("Create Event"),
|
||||
content: _t("Click <em>Continue</em> to create the event."),
|
||||
},
|
||||
{
|
||||
waitFor: 'body:has(button[data-action=save]:visible):has(.js_event)',
|
||||
title: _t("New Event Created"),
|
||||
content: _t("This is your new event page. We will edit the event presentation page."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Layout your event"),
|
||||
content: _t("Insert blocks to layout the body of your event."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(2)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag the 'Image-Text' block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
|
||||
website.EventTour = website.Tour.extend({
|
||||
id: 'event',
|
||||
name: "Create an event",
|
||||
testPath: '/event(/[0-9]+/register)?',
|
||||
init: function (editor) {
|
||||
var self = this;
|
||||
self.steps = [
|
||||
{
|
||||
title: _t("Create an Event"),
|
||||
content: _t("Let's go through the first steps to publish a new event."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip It") },
|
||||
},
|
||||
{
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Add Content"),
|
||||
content: _t("The <em>Content</em> menu allows you to create new pages, events, menus, etc."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'a[data-action=new_event]',
|
||||
placement: 'left',
|
||||
title: _t("New Event"),
|
||||
content: _t("Click here to create a new event."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal #editor_new_event input[type=text]',
|
||||
sampleText: 'Advanced Technical Training',
|
||||
placement: 'right',
|
||||
title: _t("Create an Event Name"),
|
||||
content: _t("Create a name for your new event and click <em>'Continue'</em>. e.g: Technical Training"),
|
||||
},
|
||||
{
|
||||
waitNot: '.modal input[type=text]:not([value!=""])',
|
||||
element: '.modal button.btn-primary',
|
||||
placement: 'right',
|
||||
title: _t("Create Event"),
|
||||
content: _t("Click <em>Continue</em> to create the event."),
|
||||
},
|
||||
{
|
||||
waitFor: 'body:has(button[data-action=save]:visible):has(.js_event)',
|
||||
title: _t("New Event Created"),
|
||||
content: _t("This is your new event page. We will edit the event presentation page."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Layout your event"),
|
||||
content: _t("Insert blocks to layout the body of your event."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(2)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag the 'Image-Text' block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Layout your event"),
|
||||
content: _t("Insert another block to your event."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(4)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag the 'Text Block' in your event page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save your modifications"),
|
||||
content: _t("Once you click on save, your event is updated."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: 'button[data-action=edit]:visible',
|
||||
element: 'button.btn-danger.js_publish_btn',
|
||||
placement: 'top',
|
||||
title: _t("Publish your event"),
|
||||
content: _t("Click to publish your event."),
|
||||
},
|
||||
{
|
||||
waitFor: '.js_publish_management button.js_publish_btn.btn-success:visible',
|
||||
element: '.js_publish_management button[data-toggle="dropdown"]',
|
||||
placement: 'left',
|
||||
title: _t("Customize your event"),
|
||||
content: _t("Click here to customize your event further."),
|
||||
},
|
||||
{
|
||||
element: '.js_publish_management ul>li>a:last:visible',
|
||||
},
|
||||
];
|
||||
return this._super();
|
||||
}
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Layout your event"),
|
||||
content: _t("Insert another block to your event."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(4)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag the 'Text Block' in your event page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save your modifications"),
|
||||
content: _t("Once you click on save, your event is updated."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: 'button[data-action=edit]:visible',
|
||||
element: 'button.btn-danger.js_publish_btn',
|
||||
placement: 'top',
|
||||
title: _t("Publish your event"),
|
||||
content: _t("Click to publish your event."),
|
||||
},
|
||||
{
|
||||
waitFor: '.js_publish_management button.js_publish_btn.btn-success:visible',
|
||||
element: '.js_publish_management button[data-toggle="dropdown"]',
|
||||
placement: 'left',
|
||||
title: _t("Customize your event"),
|
||||
content: _t("Click here to customize your event further."),
|
||||
},
|
||||
{
|
||||
element: '.js_publish_management ul>li>a:last:visible',
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
}());
|
||||
|
|
|
@ -2,5 +2,5 @@ import openerp.tests
|
|||
|
||||
class TestUi(openerp.tests.HttpCase):
|
||||
def test_admin(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('event')", "openerp.website.Tour")
|
||||
self.phantom_js("/", "openerp.website.Tour.run('event', 'test')", "openerp.website.Tour.tours.event")
|
||||
|
||||
|
|
|
@ -3,77 +3,63 @@
|
|||
|
||||
var website = openerp.website;
|
||||
|
||||
website.Tour.EventSaleTest = website.Tour.extend({
|
||||
id: 'event_buy_tickets',
|
||||
website.Tour.register({
|
||||
id: 'event_buy_tickets',
|
||||
name: "Try to buy tickets for event",
|
||||
path: '/event',
|
||||
init: function () {
|
||||
var self = this;
|
||||
self.steps = [
|
||||
{
|
||||
title: "select event",
|
||||
element: 'a[href*="/event"]:contains("Open Days in Los Angeles")',
|
||||
mode: 'test',
|
||||
steps: [
|
||||
{
|
||||
title: "select event",
|
||||
element: 'a[href*="/event"]:contains("Conference on Business Applications"):first',
|
||||
},
|
||||
{
|
||||
waitNot: 'a[href*="/event"]:contains("Conference on Business Applications")',
|
||||
title: "select 2 Standard tickets",
|
||||
element: 'select:eq(0)',
|
||||
sampleText: '2',
|
||||
},
|
||||
{
|
||||
title: "select 3 VIP tickets",
|
||||
waitFor: 'select:eq(0) option:contains(2):selected',
|
||||
element: 'select:eq(1)',
|
||||
sampleText: '3',
|
||||
},
|
||||
{
|
||||
title: "Order Now",
|
||||
waitFor: 'select:eq(1) option:contains(3):selected',
|
||||
element: '.btn-primary:contains("Order Now")',
|
||||
},
|
||||
{
|
||||
title: "Complete checkout",
|
||||
waitFor: '#top_menu .my_cart_quantity:contains(5)',
|
||||
element: 'form[action="/shop/confirm_order"] .btn:contains("Confirm")',
|
||||
autoComplete: function (tour) {
|
||||
if ($("input[name='name']").val() === "")
|
||||
$("input[name='name']").val("website_sale-test-shoptest");
|
||||
if ($("input[name='email']").val() === "")
|
||||
$("input[name='email']").val("website_event_sale_test_shoptest@websiteeventsaletest.optenerp.com");
|
||||
$("input[name='phone']").val("123");
|
||||
$("input[name='street']").val("123");
|
||||
$("input[name='city']").val("123");
|
||||
$("input[name='zip']").val("123");
|
||||
$("select[name='country_id']").val("21");
|
||||
},
|
||||
{
|
||||
title: "go to register page",
|
||||
waitNot: 'a[href*="/event"]:contains("Functional Webinar")',
|
||||
onload: function () {
|
||||
// use onload if website_event_track is installed
|
||||
if (!$('form:contains("Ticket Type")').size()) {
|
||||
window.location.href = $('a[href*="/event"][href*="/register"]').attr("href");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "select 2 Standard tickets",
|
||||
element: 'select[name="ticket-1"]',
|
||||
sampleText: '2',
|
||||
},
|
||||
{
|
||||
title: "select 3 VIP tickets",
|
||||
waitFor: 'select[name="ticket-1"] option:contains(2):selected',
|
||||
element: 'select[name="ticket-2"]',
|
||||
sampleText: '3',
|
||||
},
|
||||
{
|
||||
title: "Order Now",
|
||||
waitFor: 'select[name="ticket-2"] option:contains(3):selected',
|
||||
element: '.btn-primary:contains("Order Now")',
|
||||
},
|
||||
{
|
||||
title: "Complete checkout",
|
||||
waitFor: '#top_menu .my_cart_quantity:contains(5)',
|
||||
element: 'form[action="/shop/confirm_order"] .btn:contains("Confirm")',
|
||||
onload: function (tour) {
|
||||
if ($("input[name='name']").val() === "")
|
||||
$("input[name='name']").val("website_sale-test-shoptest");
|
||||
if ($("input[name='email']").val() === "")
|
||||
$("input[name='email']").val("website_event_sale_test_shoptest@websiteeventsaletest.optenerp.com");
|
||||
$("input[name='phone']").val("123");
|
||||
$("input[name='street']").val("123");
|
||||
$("input[name='city']").val("123");
|
||||
$("input[name='zip']").val("123");
|
||||
$("select[name='country_id']").val("21");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "select payment",
|
||||
element: '#payment_method label:has(img[title="transfer"]) input',
|
||||
},
|
||||
{
|
||||
title: "Pay Now",
|
||||
waitFor: '#payment_method label:has(input:checked):has(img[title="transfer"])',
|
||||
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
|
||||
},
|
||||
{
|
||||
title: "finish",
|
||||
waitFor: '.oe_website_sale:contains("Thank you for your order")',
|
||||
}
|
||||
];
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "select payment",
|
||||
element: '#payment_method label:has(img[title="Wire Transfer"]) input',
|
||||
},
|
||||
{
|
||||
title: "Pay Now",
|
||||
waitFor: '#payment_method label:has(input:checked):has(img[title="Wire Transfer"])',
|
||||
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
|
||||
},
|
||||
{
|
||||
title: "finish",
|
||||
waitFor: '.oe_website_sale:contains("Thank you for your order")',
|
||||
}
|
||||
]
|
||||
});
|
||||
// for test without editor bar
|
||||
website.Tour.add(website.Tour.EventSaleTest);
|
||||
|
||||
}());
|
||||
|
|
|
@ -1 +1 @@
|
|||
#import test_ui
|
||||
import test_ui
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import os
|
||||
|
||||
import openerp.tests
|
||||
|
||||
inject = [
|
||||
"./../../../website/static/src/js/website.tour.test.js",
|
||||
"./../../../website_event_sale/static/src/js/website.tour.event_sale.js",
|
||||
("openerp.website.Tour", os.path.join(os.path.dirname(__file__), '../../website/static/src/js/website.tour.js')),
|
||||
("openerp.website.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.event_sale.js")),
|
||||
]
|
||||
|
||||
@openerp.tests.common.at_install(False)
|
||||
@openerp.tests.common.post_install(True)
|
||||
class TestUi(openerp.tests.HttpCase):
|
||||
def test_admin(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('event_buy_tickets')", "openerp.website.Tour", inject=inject)
|
||||
self.phantom_js("/", "openerp.website.Tour.run('event_buy_tickets', 'test')", "openerp.website.Tour.tours.event_buy_tickets", inject=inject)
|
||||
|
||||
def test_demo(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('event_buy_tickets')", "openerp.website.Tour", login="demo", password="demo", inject=inject);
|
||||
self.phantom_js("/", "openerp.website.Tour.run('event_buy_tickets', 'test')", "openerp.website.Tour.tours.event_buy_tickets", login="demo", password="demo", inject=inject);
|
||||
|
||||
def test_public(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('event_buy_tickets')", "openerp.website.Tour", login=None, inject=inject);
|
||||
self.phantom_js("/", "openerp.website.Tour.run('event_buy_tickets', 'test')", "openerp.website.Tour.tours.event_buy_tickets", login=None, inject=inject);
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
<xpath expr='//t[@name="layout_head"]' position="before">
|
||||
<link rel='stylesheet' href='/website/static/src/css/snippets.css'/>
|
||||
<link rel='stylesheet' href='/website/static/src/css/editor.css'/>
|
||||
<link rel='stylesheet' href='/website/static/lib/bootstrap-tour/bootstrap-tour.css'/>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/web/static/lib/select2/select2.css"/>
|
||||
|
@ -50,7 +49,6 @@
|
|||
<script t-if="not translatable" type="text/javascript" src="/website/static/lib/ace/ace.js"></script>
|
||||
<script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js"></script>
|
||||
<script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script>
|
||||
<!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) -->
|
||||
<script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script>
|
||||
<script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script>
|
||||
|
|
|
@ -3,92 +3,87 @@
|
|||
|
||||
var website = openerp.website;
|
||||
|
||||
website.Tour.ShopTest = website.Tour.extend({
|
||||
id: 'shop_buy_product',
|
||||
website.Tour.register({
|
||||
id: 'shop_buy_product',
|
||||
name: "Try to buy products",
|
||||
path: '/shop',
|
||||
init: function () {
|
||||
var self = this;
|
||||
self.steps = [
|
||||
{
|
||||
title: "select ipod",
|
||||
element: '.oe_product_cart a:contains("iPod")',
|
||||
mode: 'test',
|
||||
steps: [
|
||||
{
|
||||
title: "select ipod",
|
||||
element: '.oe_product_cart a:contains("iPod")',
|
||||
},
|
||||
{
|
||||
title: "select ipod 32Go",
|
||||
element: 'input[name="product_id"]:not([checked])',
|
||||
},
|
||||
{
|
||||
title: "click on add to cart",
|
||||
waitFor: 'input[name="product_id"]:eq(1)[checked]',
|
||||
element: 'form[action="/shop/add_cart"] .btn',
|
||||
},
|
||||
{
|
||||
title: "add suggested",
|
||||
element: 'form[action="/shop/add_cart"] .btn-link:contains("Add to Cart")',
|
||||
},
|
||||
{
|
||||
title: "add one more iPod",
|
||||
waitFor: '.my_cart_quantity:contains(2)',
|
||||
element: '#mycart_products tr:contains("iPod: 32 Gb") a.js_add_cart_json:eq(1)',
|
||||
},
|
||||
{
|
||||
title: "remove Headphones",
|
||||
waitFor: '#mycart_products tr:contains("iPod: 32 Gb") input.js_quantity[value=2]',
|
||||
element: '#mycart_products tr:contains("Apple In-Ear Headphones") a.js_add_cart_json:first',
|
||||
},
|
||||
{
|
||||
title: "set one iPod",
|
||||
waitNot: '#mycart_products tr:contains("Apple In-Ear Headphones")',
|
||||
element: '#mycart_products input.js_quantity',
|
||||
sampleText: '1',
|
||||
},
|
||||
{
|
||||
title: "go to checkout",
|
||||
waitFor: '#mycart_products input.js_quantity[value=1]',
|
||||
element: 'a[href="/shop/checkout"]',
|
||||
},
|
||||
{
|
||||
title: "test with input error",
|
||||
element: 'form[action="/shop/confirm_order"] .btn:contains("Confirm")',
|
||||
onload: function (tour) {
|
||||
$("input[name='phone']").val("");
|
||||
},
|
||||
{
|
||||
title: "select ipod 32Go",
|
||||
element: 'input[name="product_id"]:not([checked])',
|
||||
},
|
||||
{
|
||||
title: "test without input error",
|
||||
waitFor: 'form[action="/shop/confirm_order"] .has-error',
|
||||
element: 'form[action="/shop/confirm_order"] .btn:contains("Confirm")',
|
||||
onload: function (tour) {
|
||||
if ($("input[name='name']").val() === "")
|
||||
$("input[name='name']").val("website_sale-test-shoptest");
|
||||
if ($("input[name='email']").val() === "")
|
||||
$("input[name='email']").val("website_sale_test_shoptest@websitesaletest.optenerp.com");
|
||||
$("input[name='phone']").val("123");
|
||||
$("input[name='street']").val("123");
|
||||
$("input[name='city']").val("123");
|
||||
$("input[name='zip']").val("123");
|
||||
$("select[name='country_id']").val("21");
|
||||
},
|
||||
{
|
||||
title: "click on add to cart",
|
||||
waitFor: 'input[name="product_id"]:eq(1)[checked]',
|
||||
element: 'form[action="/shop/add_cart"] .btn',
|
||||
},
|
||||
{
|
||||
title: "add suggested",
|
||||
element: 'form[action="/shop/add_cart"] .btn-link:contains("Add to Cart")',
|
||||
},
|
||||
{
|
||||
title: "add one more iPod",
|
||||
waitFor: '.my_cart_quantity:contains(2)',
|
||||
element: '#mycart_products tr:contains("iPod: 32 Gb") a.js_add_cart_json:eq(1)',
|
||||
},
|
||||
{
|
||||
title: "remove Headphones",
|
||||
waitFor: '#mycart_products tr:contains("iPod: 32 Gb") input.js_quantity[value=2]',
|
||||
element: '#mycart_products tr:contains("Apple In-Ear Headphones") a.js_add_cart_json:first',
|
||||
},
|
||||
{
|
||||
title: "set one iPod",
|
||||
waitNot: '#mycart_products tr:contains("Apple In-Ear Headphones")',
|
||||
element: '#mycart_products input.js_quantity',
|
||||
sampleText: '1',
|
||||
},
|
||||
{
|
||||
title: "go to checkout",
|
||||
waitFor: '#mycart_products input.js_quantity[value=1]',
|
||||
element: 'a[href="/shop/checkout"]',
|
||||
},
|
||||
{
|
||||
title: "test with input error",
|
||||
element: 'form[action="/shop/confirm_order"] .btn:contains("Confirm")',
|
||||
onload: function (tour) {
|
||||
$("input[name='phone']").val("");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "test without input error",
|
||||
waitFor: 'form[action="/shop/confirm_order"] .has-error',
|
||||
element: 'form[action="/shop/confirm_order"] .btn:contains("Confirm")',
|
||||
onload: function (tour) {
|
||||
if ($("input[name='name']").val() === "")
|
||||
$("input[name='name']").val("website_sale-test-shoptest");
|
||||
if ($("input[name='email']").val() === "")
|
||||
$("input[name='email']").val("website_sale_test_shoptest@websitesaletest.optenerp.com");
|
||||
$("input[name='phone']").val("123");
|
||||
$("input[name='street']").val("123");
|
||||
$("input[name='city']").val("123");
|
||||
$("input[name='zip']").val("123");
|
||||
$("select[name='country_id']").val("21");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "select payment",
|
||||
element: '#payment_method label:has(img[title="Wire Transfer"]) input',
|
||||
},
|
||||
{
|
||||
title: "Pay Now",
|
||||
waitFor: '#payment_method label:has(input:checked):has(img[title="Wire Transfer"])',
|
||||
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
|
||||
},
|
||||
{
|
||||
title: "finish",
|
||||
waitFor: '.oe_website_sale:contains("Thank you for your order")',
|
||||
}
|
||||
];
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "select payment",
|
||||
element: '#payment_method label:has(img[title="Wire Transfer"]) input',
|
||||
},
|
||||
{
|
||||
title: "Pay Now",
|
||||
waitFor: '#payment_method label:has(input:checked):has(img[title="Wire Transfer"])',
|
||||
element: '.oe_sale_acquirer_button .btn[name="submit"]:visible',
|
||||
},
|
||||
{
|
||||
title: "finish",
|
||||
waitFor: '.oe_website_sale:contains("Thank you for your order")',
|
||||
}
|
||||
]
|
||||
});
|
||||
// for test without editor bar
|
||||
website.Tour.add(website.Tour.ShopTest);
|
||||
|
||||
}());
|
||||
|
|
|
@ -4,125 +4,114 @@
|
|||
var website = openerp.website;
|
||||
var _t = openerp._t;
|
||||
|
||||
website.EditorBar.include({
|
||||
start: function () {
|
||||
this.registerTour(new website.Tour.Shop(this));
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
|
||||
website.Tour.Shop = website.Tour.extend({
|
||||
website.Tour.register({
|
||||
id: 'shop',
|
||||
name: "Create a product",
|
||||
testPath: '/shop',
|
||||
init: function () {
|
||||
var self = this;
|
||||
self.steps = [
|
||||
{
|
||||
title: _t("Welcome to your shop"),
|
||||
content: _t("You successfully installed the e-commerce. This guide will help you to create your product and promote your sales."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip It") },
|
||||
},
|
||||
{
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Create your first product"),
|
||||
content: _t("Click here to add a new product."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'a[data-action=new_product]',
|
||||
placement: 'left',
|
||||
title: _t("Create a new product"),
|
||||
content: _t("Select 'New Product' to create it and manage its properties to boost your sales."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal #editor_new_product input[type=text]',
|
||||
sampleText: 'New Product',
|
||||
placement: 'right',
|
||||
title: _t("Choose name"),
|
||||
content: _t("Enter a name for your new product then click 'Continue'."),
|
||||
},
|
||||
{
|
||||
waitNot: '.modal input[type=text]:not([value!=""])',
|
||||
element: '.modal button.btn-primary',
|
||||
placement: 'right',
|
||||
title: _t("Create Product"),
|
||||
content: _t("Click <em>Continue</em> to create the product."),
|
||||
},
|
||||
{
|
||||
waitFor: 'body:has(button[data-action=save]:visible):has(.js_sale)',
|
||||
title: _t("New product created"),
|
||||
content: _t("This page contains all the information related to the new product."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
element: '.product_price .oe_currency_value',
|
||||
sampleText: '20.50',
|
||||
placement: 'left',
|
||||
title: _t("Change the price"),
|
||||
content: _t("Edit the price of this product by clicking on the amount."),
|
||||
},
|
||||
{
|
||||
waitNot: '.product_price .oe_currency_value:containsExact(1.00)',
|
||||
element: '#wrap img.product_detail_img',
|
||||
placement: 'top',
|
||||
title: _t("Update image"),
|
||||
content: _t("Click here to set an image describing your product."),
|
||||
},
|
||||
{
|
||||
element: 'img[alt=ipad]',
|
||||
placement: 'top',
|
||||
title: _t("Select an Image"),
|
||||
content: _t("Let's select an ipad image."),
|
||||
},
|
||||
{
|
||||
waitFor: '.media_selected img[alt=ipad]',
|
||||
element: '.modal-content button.save',
|
||||
placement: 'top',
|
||||
title: _t("Save this Image"),
|
||||
content: _t("Click on save to add the image to the product decsription."),
|
||||
},
|
||||
{
|
||||
waitNot: '.modal-content:visible',
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Describe the Product"),
|
||||
content: _t("Insert blocks like text-image, or gallery to fully describe the product."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(7)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag the 'Big Picture' block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save your modifications"),
|
||||
content: _t("Once you click on save, your product is updated."),
|
||||
popover: { fixed: true },
|
||||
name: _t("Create a product"),
|
||||
steps: [
|
||||
{
|
||||
title: _t("Welcome to your shop"),
|
||||
content: _t("You successfully installed the e-commerce. This guide will help you to create your product and promote your sales."),
|
||||
popover: { next: _t("Start Tutorial"), end: _t("Skip It") },
|
||||
},
|
||||
{
|
||||
element: '#content-menu-button',
|
||||
placement: 'left',
|
||||
title: _t("Create your first product"),
|
||||
content: _t("Click here to add a new product."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'a[data-action=new_product]',
|
||||
placement: 'left',
|
||||
title: _t("Create a new product"),
|
||||
content: _t("Select 'New Product' to create it and manage its properties to boost your sales."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: '.modal #editor_new_product input[type=text]',
|
||||
sampleText: 'New Product',
|
||||
placement: 'right',
|
||||
title: _t("Choose name"),
|
||||
content: _t("Enter a name for your new product then click 'Continue'."),
|
||||
},
|
||||
{
|
||||
waitNot: '.modal input[type=text]:not([value!=""])',
|
||||
element: '.modal button.btn-primary',
|
||||
placement: 'right',
|
||||
title: _t("Create Product"),
|
||||
content: _t("Click <em>Continue</em> to create the product."),
|
||||
},
|
||||
{
|
||||
waitFor: 'body:has(button[data-action=save]:visible):has(.js_sale)',
|
||||
title: _t("New product created"),
|
||||
content: _t("This page contains all the information related to the new product."),
|
||||
popover: { next: _t("Continue") },
|
||||
},
|
||||
{
|
||||
element: '.product_price .oe_currency_value',
|
||||
sampleText: '20.50',
|
||||
placement: 'left',
|
||||
title: _t("Change the price"),
|
||||
content: _t("Edit the price of this product by clicking on the amount."),
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
waitFor: '#website-top-navbar button[data-action="edit"]:visible',
|
||||
element: '.js_publish_management button.js_publish_btn.btn-danger',
|
||||
placement: 'top',
|
||||
title: _t("Publish your product"),
|
||||
content: _t("Click to publish your product so your customers can see it."),
|
||||
},
|
||||
{
|
||||
waitFor: '.js_publish_management button.js_publish_btn.btn-success:visible',
|
||||
title: _t("Congratulations"),
|
||||
content: _t("Congratulations! You just created and published your first product."),
|
||||
popover: { next: _t("Close Tutorial") },
|
||||
},
|
||||
];
|
||||
return this._super();
|
||||
}
|
||||
|
||||
{
|
||||
waitNot: '.product_price .oe_currency_value:containsExact(1.00)',
|
||||
element: '#wrap img.product_detail_img',
|
||||
placement: 'top',
|
||||
title: _t("Update image"),
|
||||
content: _t("Click here to set an image describing your product."),
|
||||
},
|
||||
{
|
||||
element: 'img[alt=ipad]',
|
||||
placement: 'top',
|
||||
title: _t("Select an Image"),
|
||||
content: _t("Let's select an ipad image."),
|
||||
},
|
||||
{
|
||||
waitFor: '.media_selected img[alt=ipad]',
|
||||
element: '.modal-content button.save',
|
||||
placement: 'top',
|
||||
title: _t("Save this Image"),
|
||||
content: _t("Click on save to add the image to the product decsription."),
|
||||
},
|
||||
{
|
||||
waitNot: '.modal-content:visible',
|
||||
element: 'button[data-action=snippet]',
|
||||
placement: 'bottom',
|
||||
title: _t("Describe the Product"),
|
||||
content: _t("Insert blocks like text-image, or gallery to fully describe the product."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
snippet: '#snippet_structure .oe_snippet:eq(7)',
|
||||
placement: 'bottom',
|
||||
title: _t("Drag & Drop a block"),
|
||||
content: _t("Drag the 'Big Picture' block and drop it in your page."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
element: 'button[data-action=save]',
|
||||
placement: 'right',
|
||||
title: _t("Save your modifications"),
|
||||
content: _t("Once you click on save, your product is updated."),
|
||||
popover: { fixed: true },
|
||||
},
|
||||
{
|
||||
waitFor: '#website-top-navbar button[data-action="edit"]:visible',
|
||||
element: '.js_publish_management button.js_publish_btn.btn-danger',
|
||||
placement: 'top',
|
||||
title: _t("Publish your product"),
|
||||
content: _t("Click to publish your product so your customers can see it."),
|
||||
},
|
||||
{
|
||||
waitFor: '.js_publish_management button.js_publish_btn.btn-success:visible',
|
||||
title: _t("Congratulations"),
|
||||
content: _t("Congratulations! You just created and published your first product."),
|
||||
popover: { next: _t("Close Tutorial") },
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
}());
|
||||
|
|
|
@ -11,13 +11,13 @@ inject = [
|
|||
@openerp.tests.common.post_install(True)
|
||||
class TestUi(openerp.tests.HttpCase):
|
||||
def test_01_admin_shop_tour(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('shop')", "openerp.website.Tour.Shop", login="admin")
|
||||
self.phantom_js("/", "openerp.website.Tour.run('shop', 'test')", "openerp.website.Tour.tours.shop", login="admin")
|
||||
|
||||
def test_02_admin_checkout(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('shop_buy_product')", "openerp.website.Tour.ShopTest", login="admin", inject=inject)
|
||||
self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="admin", inject=inject)
|
||||
|
||||
def test_03_demo_checkout(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('shop_buy_product')", "openerp.website.Tour.ShopTest", login="demo", inject=inject)
|
||||
self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="demo", inject=inject)
|
||||
|
||||
def test_04_public_checkout(self):
|
||||
self.phantom_js("/", "openerp.website.Tour.run_test('shop_buy_product')", "openerp.website.Tour.ShopTest", inject=inject)
|
||||
self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", inject=inject)
|
||||
|
|
Loading…
Reference in New Issue