[MERGE] merged with trunk up to revision 9381

bzr revid: qdp-launchpad@openerp.com-20140425090414-vpg4szws9y4fgznc
This commit is contained in:
Quentin (OpenERP) 2014-04-25 11:04:14 +02:00
commit 7a6b00b651
97 changed files with 1938 additions and 3241 deletions

View File

@ -430,6 +430,19 @@ class account_move_line(osv.osv):
elif line.reconcile_partial_id:
res[line.id] = str(line.reconcile_partial_id.name)
return res
def _get_move_from_reconcile(self, cr, uid, ids, context=None):
move = {}
for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
for line in r.line_partial_ids:
move[line.move_id.id] = True
for line in r.line_id:
move[line.move_id.id] = True
move_line_ids = []
if move:
move_line_ids = self.pool.get('account.move.line').search(cr, uid, [('journal_id','in',move.keys())], context=context)
return move_line_ids
_columns = {
'name': fields.char('Name', size=64, required=True),
@ -445,7 +458,8 @@ class account_move_line(osv.osv):
'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=1),
'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=2),
'reconcile_partial_id': fields.many2one('account.move.reconcile', 'Partial Reconcile', readonly=True, ondelete='set null', select=2),
'reconcile': fields.function(_get_reconcile, type='char', string='Reconcile Ref'),
'reconcile': fields.function(_get_reconcile, type='char', string='Reconcile Ref', store={
'account.move.line': (lambda self, cr, uid, ids, c={}: ids, ['reconcile_id','reconcile_partial_id'], 50),'account.move.reconcile': (_get_move_from_reconcile, None, 50)}),
'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')),
'amount_residual_currency': fields.function(_amount_residual, string='Residual Amount in Currency', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in its currency (maybe different of the company currency)."),
'amount_residual': fields.function(_amount_residual, string='Residual Amount', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in the company currency."),

View File

@ -1628,6 +1628,7 @@
<field name="name"/>
<field name="active"/>
</group>
<label for="note"/>
<field name="note" placeholder="Payment term explanation for the customer..."/>
<separator string="Computation"/>
<field name="line_ids"/>
@ -2443,7 +2444,7 @@
<field name="sign"/>
<field name="style_overwrite"/>
</group>
<notebook>
<notebook attrs="{'invisible': [('type','not in',['accounts','account_type'])]}">
<page string="Report">
<group>
<field name="display_detail" attrs="{'invisible': [('type','not in',['accounts','account_type'])]}"/>

View File

@ -29,7 +29,7 @@ Module to automate letters for unpaid invoices, with multi-level recalls.
You can define your multiple levels of recall through the menu:
---------------------------------------------------------------
Configuration / Follow-Up Levels
Configuration / Follow-up / Follow-up Levels
Once it is defined, you can automatically print recalls every day through simply clicking on the menu:
------------------------------------------------------------------------------------------------------
@ -55,6 +55,7 @@ Note that if you want to check the follow-up level for a given partner/account e
'account_followup_view.xml',
'account_followup_customers.xml',
'wizard/account_followup_print_view.xml',
'res_config_view.xml',
'views/report_followup.xml',
'account_followup_reports.xml'
],

View File

@ -30,7 +30,7 @@ class followup(osv.osv):
_columns = {
'followup_line': fields.one2many('account_followup.followup.line', 'followup_id', 'Follow-up'),
'company_id': fields.many2one('res.company', 'Company', required=True),
'name': fields.related('company_id', 'name', string = "Name"),
'name': fields.related('company_id', 'name', string = "Name", readonly=True, type="char"),
}
_defaults = {
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'account_followup.followup', context=c),
@ -479,4 +479,20 @@ class res_partner(osv.osv):
fnct_search=_payment_earliest_date_search),
}
class account_config_settings(osv.TransientModel):
_name = 'account.config.settings'
_inherit = 'account.config.settings'
def open_followup_level_form(self, cr, uid, ids, context=None):
res_ids = self.pool.get('account_followup.followup').search(cr, uid, [], context=context)
return {
'type': 'ir.actions.act_window',
'name': 'Payment Follow-ups',
'res_model': 'account_followup.followup',
'res_id': res_ids and res_ids[0] or False,
'view_mode': 'form,tree',
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -50,11 +50,11 @@
<filter string="No Responsible" domain="[('payment_responsible_id', '=', False)]"/>
<filter string="My Follow-ups" domain="[('payment_responsible_id','=', uid)]" name="my"/>
</group>
<group expand="1" string="Group By...">
<filter string="Follow-up Responsible" context="{'group_by':'payment_responsible_id'}"/>
<filter string="Followup Level" context="{'group_by':'latest_followup_level_id'}"/>
</group>
</search>
<group string="Group By..." position="inside">
<filter string="Follow-up Responsible" context="{'group_by':'payment_responsible_id'}"/>
<filter string="Followup Level" context="{'group_by':'latest_followup_level_id'}"/>
</group>
</field>
</record>

View File

@ -71,7 +71,9 @@
<field name="model">account_followup.followup</field>
<field name="arch" type="xml">
<form string="Follow-up" version="7.0">
<h1><field name="company_id" widget="selection" class="oe_inline"/></h1>
<h1><field name="name"/></h1>
<label for="company_id" groups="base.group_multi_company"/>
<field name="company_id" widget="selection" class="oe_inline" groups="base.group_multi_company"/>
<p class="oe_grey">
To remind customers of paying their invoices, you can
define different actions depending on how severely
@ -113,6 +115,7 @@
<field name="res_model">account_followup.followup</field>
<field name="search_view_id" ref="view_account_followup_filter"/>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define follow-up levels and their related actions.
@ -130,8 +133,11 @@
parent="menu_finance_followup"
sequence="0"
id="menu_manual_reconcile_followup"/>
<menuitem id="account_followup_main_menu" parent="account.menu_finance_configuration" name="Follow-up"/>
<menuitem action="action_account_followup_definition_form" id="account_followup_menu" parent="account.menu_finance_configuration" name="Follow-up Levels"/>
<menuitem action="action_account_followup_definition_form" id="account_followup_menu" parent="account_followup_main_menu" name="Follow-up Levels"/>
<record id="account_move_line_partner_tree" model="ir.ui.view">
<field name="name">account.move.line.partner.tree</field>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_account_config_settings_inherit" model="ir.ui.view">
<field name="name">account settings</field>
<field name="model">account.config.settings</field>
<field name="inherit_id" ref="account.view_account_config_settings"/>
<field name="arch" type="xml">
<xpath expr="//label[@for='module_account_followup']" position="after">
<button type="object" name="open_followup_level_form" string="Configure your follow-up levels" class="oe_link"/>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -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:

View File

@ -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:

View File

@ -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 users ID for security checks,
@param ids: List of Auddittrail Rules 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 users ID for security checks,
@param ids: List of Auddittrail Rules 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 users 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 users 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 users 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 users 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 users 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 users 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:

View File

@ -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>

View File

@ -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>

View File

@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_audittrail_rule_all_users audittrail rule all model_audittrail_rule base.group_system 1 1 1 0
3 access_audittrail_rule_all_access audittrail rule all model_audittrail_rule base.group_erp_manager 1 1 1 0
4 access_audittrail_log_all_users audittrail log all model_audittrail_log base.group_user 1 0 1 0
5 access_audittrail_log_line_all_users audittrail log line all model_audittrail_log_line base.group_user 1 0 1 0

View File

@ -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 audittrail_view_log
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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 users ID for security checks,
@param ids: List of audittrail view logs 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:

View File

@ -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>

View File

@ -153,7 +153,7 @@
<field name="user_id"/>
<field name="note"/>
<field name="code"/>
<filter name="personal" string="My Salesteams" domain="['|', ('member_ids', '=', uid), ('user_id', '=', uid)]"/>
<filter name="personal" string="My Sales Teams" domain="['|', ('member_ids', '=', uid), ('user_id', '=', uid)]"/>
<group expand="0" string="Group By...">
<filter string="Team Leader" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Parent Sales Teams" domain="[]" context="{'group_by':'parent_id'}"/>

View File

@ -361,8 +361,8 @@
<filter string="Last Post (weekly)" name="group_message_last_post" domain="[]" context="{'group_by':'message_last_post:week'}"/>
</group>
<group string="Display">
<filter string="Show Countries" context="{'invisible_country': False}" help="Show Countries"/>
<filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team" groups="base.group_multi_salesteams"/>
<filter string="Countries" context="{'invisible_country': False}" help="Countries"/>
<filter string="Sales Team" context="{'invisible_section': False}" domain="[]" help="Sales Team" groups="base.group_multi_salesteams"/>
</group>
</search>
</field>
@ -581,8 +581,8 @@
<filter string="Last Post (weekly)" name="group_message_last_post" domain="[]" context="{'group_by':'message_last_post:week'}"/>
</group>
<group string="Display">
<filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team" groups="base.group_multi_salesteams"/>
<filter string="Show Countries" context="{'invisible_country': False}" help="Show Countries"/>
<filter string="Sales Team" context="{'invisible_section': False}" domain="[]" help="Sales Team" groups="base.group_multi_salesteams"/>
<filter string="Countries" context="{'invisible_country': False}" help="Countries"/>
</group>
</search>
</field>

View File

@ -33,7 +33,6 @@
<graph string="Leads Analysis" type="pivot" stacked="True">
<field name="date_deadline" type="row"/>
<field name="user_id" type="col"/>
<field name="stage_id" type="col"/>
<field name="planned_revenue" type="measure"/>
</graph>
</field>

View File

@ -216,7 +216,7 @@ class delivery_grid(osv.osv):
ok = True
break
if not ok:
raise osv.except_osv(_('No price available!'), _('No line matched this product or order in the chosen delivery grid.'))
raise osv.except_osv(_("Unable to fetch delivery method!"), _("Selected product in the delivery method doesn't fulfill any of the delivery grid(s) criteria."))
return price

View File

@ -41,9 +41,7 @@
<field name="free_if_more_than" attrs="{'readonly':[('use_detailed_pricelist', '=', True)]}"/>
<field name="amount" attrs="{'required':[('free_if_more_than','&lt;&gt;',False)], 'invisible':[('free_if_more_than','=',False)]}"/>
</div>
<newline/>
<field name="use_detailed_pricelist"/>
<field name="use_detailed_pricelist"/>
</group>
</group>
@ -142,7 +140,7 @@
</page>
<page string="Destination">
<group string="Countries">
<field name="country_ids"/>
<field name="country_ids" nolabel="1"/>
</group>
<group string="States">
<field colspan="2" name="state_ids" nolabel="1"/>

View File

@ -164,7 +164,7 @@
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="user_domain">[('groups_id', 'in', ref('base.group_user'))]</field>
<field name="user_domain" eval="[('groups_id', '=', ref('base.group_user'))]" />
<field name="state">inprogress</field>
<field name="category">other</field>
</record>
@ -174,7 +174,7 @@
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="user_domain">[('groups_id', 'in', ref('base.user_root'))]</field>
<field name="user_domain" eval="[('groups_id', '=', ref('base.group_erp_manager'))]" />
<field name="state">inprogress</field>
<field name="category">other</field>
</record>

View File

@ -21,14 +21,13 @@
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT as DF
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from datetime import date, datetime, timedelta
import calendar
import logging
import functools
_logger = logging.getLogger(__name__)
# display top 3 in ranking, could be db variable
@ -117,12 +116,6 @@ class gamification_challenge(osv.Model):
except ValueError:
return False
def _get_challenger_users(self, cr, uid, domain, context=None):
ref = functools.partial(self.pool['ir.model.data'].xmlid_to_res_id, cr, uid)
user_domain = eval(domain, {'ref': ref})
return self.pool['res.users'].search(cr, uid, user_domain, context=context)
_order = 'end_date, start_date, name, id'
_columns = {
'name': fields.char('Challenge Name', required=True, translate=True),
@ -218,7 +211,6 @@ class gamification_challenge(osv.Model):
def create(self, cr, uid, vals, context=None):
"""Overwrite the create method to add the user of groups"""
# add users when change the group auto-subscription
if vals.get('user_domain'):
user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
@ -240,14 +232,18 @@ class gamification_challenge(osv.Model):
if isinstance(ids, (int,long)):
ids = [ids]
if vals.get('state') == 'inprogress':
for challenge in self.browse(cr, uid, ids, context=context):
user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
write_op = [(4, user_id) for user_id in user_ids]
self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
self.message_subscribe_users(cr, uid, [challenge.id], user_ids, context=context)
if vals.get('user_domain'):
user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
self.generate_goals_from_challenge(cr, uid, ids, context=context)
if not vals.get('user_ids'):
vals['user_ids'] = []
vals['user_ids'] += [(4, user_id) for user_id in user_ids]
write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
if vals.get('state') == 'inprogress':
self._recompute_challenge_users(cr, uid, ids, context=context)
self._generate_goals_from_challenge(cr, uid, ids, context=context)
elif vals.get('state') == 'done':
self.check_challenge_reward(cr, uid, ids, force=True, context=context)
@ -256,9 +252,6 @@ class gamification_challenge(osv.Model):
# resetting progress
if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context):
raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
return write_res
@ -314,18 +307,10 @@ class gamification_challenge(osv.Model):
# update every running goal already generated linked to selected challenges
goal_obj.update(cr, uid, goal_ids, context=context)
self._recompute_challenge_users(cr, uid, ids, context=context)
self._generate_goals_from_challenge(cr, uid, ids, context=context)
for challenge in self.browse(cr, uid, ids, context=context):
# in case of new users matching the domain
old_user_ids = [user.id for user in challenge.user_ids]
new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
to_remove_ids = list(set(old_user_ids) - set(new_user_ids))
to_add_ids = list(set(new_user_ids) - set(old_user_ids))
write_op = [(3, user_id) for user_id in to_remove_ids]
write_op += [(4, user_id) for user_id in to_add_ids]
self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
# goals closed but still opened at the last report date
closed_goals_to_report = goal_obj.search(cr, uid, [
@ -345,11 +330,37 @@ class gamification_challenge(osv.Model):
return True
def quick_update(self, cr, uid, challenge_id, context=None):
"""Update all the goals of a challenge, no generation of new goals"""
"""Update all the goals of a specific challenge, no generation of new goals"""
goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
return True
def _get_challenger_users(self, cr, uid, domain, context=None):
user_domain = eval(ustr(domain))
return self.pool['res.users'].search(cr, uid, user_domain, context=context)
def _recompute_challenge_users(self, cr, uid, challenge_ids, context=None):
"""Recompute the domain to add new users and remove the one no longer matching the domain"""
for challenge in self.browse(cr, uid, challenge_ids, context=context):
if challenge.user_domain:
old_user_ids = [user.id for user in challenge.user_ids]
new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
to_remove_ids = list(set(old_user_ids) - set(new_user_ids))
to_add_ids = list(set(new_user_ids) - set(old_user_ids))
write_op = [(3, user_id) for user_id in to_remove_ids]
write_op += [(4, user_id) for user_id in to_add_ids]
if write_op:
self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
if to_remove_ids:
self.message_unsubscribe_users(cr, uid, [challenge.id], to_remove_ids, context=None)
if to_add_ids:
self.message_subscribe_users(cr, uid, [challenge.id], to_add_ids, context=context)
return True
def action_check(self, cr, uid, ids, context=None):
"""Check a challenge
@ -370,12 +381,17 @@ class gamification_challenge(osv.Model):
##### Automatic actions #####
def generate_goals_from_challenge(self, cr, uid, ids, context=None):
_logger.warning("Deprecated, use private method _generate_goals_from_challenge(...) instead.")
return self._generate_goals_from_challenge(cr, uid, ids, context=context)
def _generate_goals_from_challenge(self, cr, uid, ids, context=None):
"""Generate the goals for each line and user.
If goals already exist for this line and user, the line is skipped. This
can be called after each change in the list of users or lines.
:param list(int) ids: the list of challenge concerned"""
goal_obj = self.pool.get('gamification.goal')
to_update = []
for challenge in self.browse(cr, uid, ids, context=context):
(start_date, end_date) = start_end_date_for_period(challenge.period)
@ -387,45 +403,49 @@ class gamification_challenge(osv.Model):
end_date = challenge.end_date
for line in challenge.line_ids:
# FIXME: allow to restrict to a subset of users
for user in challenge.user_ids:
goal_obj = self.pool.get('gamification.goal')
domain = [('line_id', '=', line.id), ('user_id', '=', user.id)]
if start_date:
domain.append(('start_date', '=', start_date))
# there is potentially a lot of users
# detect the ones with no goal linked to this line
date_clause = ""
query_params = [line.id]
if start_date:
date_clause += "AND g.start_date = %s"
query_params.append(start_date)
if end_date:
date_clause += "AND g.end_date = %s"
query_params.append(end_date)
query = """SELECT u.id AS user_id
FROM res_users u
LEFT JOIN gamification_goal g
ON (u.id = g.user_id)
WHERE line_id = %s
{date_clause}
""".format(date_clause=date_clause)
# goal already existing for this line ?
if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
cr.execute(query, query_params)
user_with_goal_ids = cr.dictfetchall()
user_without_goal_ids = list(set([user.id for user in challenge.user_ids]) - set([user['user_id'] for user in user_with_goal_ids]))
# resume canceled goals
domain.append(('state', '=', 'canceled'))
canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
if canceled_goal_ids:
goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
to_update.extend(canceled_goal_ids)
values = {
'definition_id': line.definition_id.id,
'line_id': line.id,
'target_goal': line.target_goal,
'state': 'inprogress',
}
# skip to next user
continue
if start_date:
values['start_date'] = start_date
if end_date:
values['end_date'] = end_date
values = {
'definition_id': line.definition_id.id,
'line_id': line.id,
'user_id': user.id,
'target_goal': line.target_goal,
'state': 'inprogress',
}
if challenge.remind_update_delay:
values['remind_update_delay'] = challenge.remind_update_delay
if start_date:
values['start_date'] = start_date
if end_date:
values['end_date'] = end_date
if challenge.remind_update_delay:
values['remind_update_delay'] = challenge.remind_update_delay
new_goal_id = goal_obj.create(cr, uid, values, context=context)
to_update.append(new_goal_id)
for user_id in user_without_goal_ids:
values.update({'user_id': user_id})
goal_id = goal_obj.create(cr, uid, values, context=context)
to_update.append(goal_id)
goal_obj.update(cr, uid, to_update, context=context)
@ -638,7 +658,7 @@ class gamification_challenge(osv.Model):
message = "%s has joined the challenge" % user.name
self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
return self.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
return self._generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
# TODO in trunk, remove unused parameter user_id
def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):

View File

@ -19,7 +19,6 @@
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.osv import osv
from challenge import MAX_VISIBILITY_RANKING

View File

@ -48,6 +48,7 @@
<label for="user_domain" class="oe_edit_only" string="Assign Challenge To"/>
<div>
<field name="user_domain" widget="char_domain" options="{'model': 'res.users'}" />
<field name="user_ids" groups="base.group_no_one" widget="many2many_tags" />
</div>
</div>

View File

@ -130,7 +130,7 @@
<field name="name">Monthly Sales Targets</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="user_domain">[('groups_id', 'in', ref('base.group_sale_salesman'))]</field>
<field name="user_domain" eval="[('groups_id', 'in', ref('base.group_sale_salesman'))]" />
<field name="report_message_frequency">weekly</field>
</record>
@ -138,7 +138,7 @@
<field name="name">Lead Acquisition</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="user_domain">[('groups_id', 'in', ref('base.group_sale_salesman'))]</field>
<field name="user_domain" eval="[('groups_id', 'in', ref('base.group_sale_salesman'))]" />
<field name="report_message_frequency">weekly</field>
</record>

View File

@ -203,7 +203,7 @@
</record>
<record id="hr_expense_product" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="name">Expense Categories</field>
<field name="res_model">product.product</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>

View File

@ -500,11 +500,15 @@
<field name="name">hr.contribution.register.form</field>
<field name="model">hr.contribution.register</field>
<field name="arch" type="xml">
<form string="Contribution">
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<separator string="Description"/>
<field name="note" nolabel="1"/>
<form string="Contribution" version="7.0">
<group>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" widget="selection" class="oe_inline"/>
</group>
<newline/>
<group>
<separator string="Description"/><newline/>
<field name="note" nolabel="1"/></group>
</form>
</field>
</record>

1251
addons/hr_payroll/i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@
<graph string="Recruitment Analysis" type="pivot">
<field name="stage_id" type="row"/>
<field name="date_create" interval="week" type="col"/>
<field name="date_create" interval="day" type="col"/>
</graph>
</field>
</record>

View File

@ -65,7 +65,7 @@ class hr_timesheet_sheet(osv.osv):
def create(self, cr, uid, vals, *args, **argv):
if 'employee_id' in vals:
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).user_id:
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must assign it to a user.'))
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link him/her to a user.'))
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).product_id:
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product, like \'Consultant\'.'))
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).journal_id:
@ -76,7 +76,7 @@ class hr_timesheet_sheet(osv.osv):
if 'employee_id' in vals:
new_user_id = self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).user_id.id or False
if not new_user_id:
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must assign it to a user.'))
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link him/her to a user.'))
if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id):
raise osv.except_osv(_('Error!'), _('You cannot have 2 timesheets that overlap!\nYou should use the menu \'My Timesheet\' to avoid this problem.'))
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).product_id:

View File

@ -21,12 +21,12 @@
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base.view_partner_bank_form" />
<field name="arch" type="xml">
<field name="acc_number" position="before">
<field name="bank_code" />
<field name="office" />
<field name="acc_number" position="after">
<field name="bank_code" attrs="{'invisible':[('state','!=','rib')]}"/>
<field name="office" attrs="{'invisible':[('state','!=','rib')]}"/>
<newline />
<field name="rib_acc_number" />
<field name="key" />
<field name="rib_acc_number" attrs="{'invisible':[('state','!=','rib')]}"/>
<field name="key" attrs="{'invisible':[('state','!=','rib')]}"/>
<newline />
</field>
</field>

View File

@ -427,10 +427,14 @@
</header>
<sheet>
<group>
<field name='name'/>
<field name='category_id'/>
<field name='supplier'/>
<field name='price'/>
<group>
<field name='name'/>
<field name='category_id'/>
</group>
<group>
<field name='supplier'/>
<field name='price'/>
</group>
</group>
<label for='description'/>
<field name='description'/>
@ -461,7 +465,7 @@
<sheet>
<group string="Schedule Date">
<group>
<field name="alter_type"/>
<field name="alter_type" class="oe_inline"/>
<field name="specific_day" attrs="{'invisible': [('alter_type','!=','specific')], 'required':[('alter_type','=','specific')]}"/>
</group>
</group>

View File

@ -109,8 +109,7 @@ class mail_notification(osv.Model):
Administrator
</p>
<div>
<small>Sent by <a ...>Your Company</a> using <a ...>OpenERP</a>.</small> OR
<small>Sent by Administrator using <a ...>OpenERP</a>.</small>
<small>Sent from <a ...>Your Company</a> using <a ...>OpenERP</a>.</small>
</div>
"""
footer = ""
@ -132,7 +131,7 @@ class mail_notification(osv.Model):
company = "<a style='color:inherit' href='%s'>%s</a>" % (website_url, user.company_id.name)
else:
company = user.company_id.name
sent_by = _('Sent by %(company)s using %(openerp)s.')
sent_by = _('Sent from %(company)s using %(openerp)s')
signature_company = '<small>%s</small>' % (sent_by % {
'company': company,
'openerp': "<a style='color:inherit' href='https://www.openerp.com/'>OpenERP</a>"

View File

@ -147,6 +147,8 @@ class mail_mail(osv.Model):
def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
"""Generate URLs for links in mails: partner has access (is user):
link to action_mail_redirect action that will redirect to doc or Inbox """
if context is None:
context = {}
if partner and partner.user_ids:
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# the parameters to encode for the query and fragment part of url
@ -161,7 +163,7 @@ class mail_mail(osv.Model):
fragment.update(model=mail.model, res_id=mail.res_id)
url = urljoin(base_url, "/web?%s#%s" % (urlencode(query), urlencode(fragment)))
return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small></span>""") % url
return _("""<span class='oe_mail_footer_access'><small>about <a style='color:inherit' href="%s">%s %s</a></small></span>""") % (url, context.get('model_name', ''), mail.record_name)
else:
return None
@ -233,10 +235,19 @@ class mail_mail(osv.Model):
email sending process has failed
:return: True
"""
if context is None:
context = {}
ir_mail_server = self.pool.get('ir.mail_server')
for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
try:
# TDE note: remove me when model_id field is present on mail.message - done here to avoid doing it multiple times in the sub method
if mail.model:
model_id = self.pool['ir.model'].search(cr, SUPERUSER_ID, [('model', '=', mail.model)], context=context)[0]
model = self.pool['ir.model'].browse(cr, SUPERUSER_ID, model_id, context=context)
else:
model = None
if model:
context['model_name'] = model.name
# handle attachments
attachments = []
for attach in mail.attachment_ids:

View File

@ -25,16 +25,14 @@ class TestMassMailing(osv.TransientModel):
test_emails = tools.email_split(wizard.email_to)
mail_ids = []
for test_mail in test_emails:
body = mailing.body_html
unsubscribe_url = self.pool['mail.mass_mailing'].get_unsubscribe_url(cr, uid, mailing.id, 0, email=test_mail, context=context)
body = tools.append_content_to_html(body, unsubscribe_url, plaintext=False, container_tag='p')
mail_values = {
'email_from': mailing.email_from,
'reply_to': mailing.reply_to,
'email_to': test_mail,
'subject': mailing.name,
'body_html': body,
'body_html': mailing.body_html,
'auto_delete': True,
'mailing_id': wizard.mass_mailing_id.id,
}
mail_ids.append(Mail.create(cr, uid, mail_values, context=context))
Mail.send(cr, uid, mail_ids, context=context)

View File

@ -39,8 +39,8 @@ class mail_mail(osv.Model):
if partner and not partner.user_ids:
contex_signup = dict(context, signup_valid=True)
signup_url = partner_obj._get_signup_url_for_action(cr, SUPERUSER_ID, [partner.id],
model=mail.model, res_id=mail.res_id,
context=contex_signup)[partner.id]
return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents through <a style='color:inherit' href="%s">our Customer Portal</a></small></span>""") % signup_url
model=mail.model, res_id=mail.res_id,
context=contex_signup)[partner.id]
return _(""", <span class='oe_mail_footer_access'><small>access %s %s through <a style='color:inherit' href="%s">our Customer Portal</a></small></span>""") % (context.get('model_name', ''), mail.record_name, signup_url)
else:
return super(mail_mail, self)._get_partner_access_link(cr, uid, mail, partner=partner, context=context)

View File

@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_issues project_phase project_long_term.model_project_phase base.group_portal 1 0 0 0
3 access_issues_public project_phase_public project_long_term.model_project_phase base.group_public 1 0 0 0

View File

@ -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>

View File

@ -12,15 +12,23 @@
<field name="model">product.pricelist.version</field>
<field name="arch" type="xml">
<form string="Pricelist Version" version="7.0">
<group col="4">
<field name="name"/>
<field name="active"/>
<field colspan="4" name="pricelist_id"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<group>
<group>
<field name="name"/>
<field name="pricelist_id"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
</group>
<group>
<field name="date_start"/>
<field name="date_end"/>
<field name="active"/>
</group>
</group>
<field name="items_id"/>
<notebook>
<page string="Item List">
<field name="items_id"/>
</page>
</notebook>
</form>
</field>
</record>

View File

@ -576,6 +576,7 @@
<field name="model">product.ul</field>
<field name="arch" type="xml">
<form string="Logistic Units" version="7.0">
<sheet>
<group>
<field name="name"/>
<field name="type"/>
@ -586,6 +587,7 @@
<field name="length"/>
<field name="weight"/>
</group>
</sheet>
</form>
</field>
</record>

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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>

View File

@ -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>

View File

@ -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="Show Context" name="context_show" context="{'context_show': True}" domain="[]" icon="terp-camera_test" help="Show the context field"/>
<filter string="Show 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>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_project_gtd_context_user project.gtd.context project user model_project_gtd_context project.group_project_user 1 0 0 0
3 access_project_gtd_timebox_user project.gtd.timebox project user model_project_gtd_timebox project.group_project_user 1 0 0 0
4 access_project_gtd_context_manager project.gtd.context project manager model_project_gtd_context project.group_project_manager 1 1 1 1
5 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

View File

@ -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"

View File

@ -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:

View File

@ -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:

View File

@ -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>

View File

@ -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:

View File

@ -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>

View File

@ -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:

View File

@ -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:

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_project_phase project.phase model_project_phase project.group_project_user 1 1 1 0
3 access_project_user_allocation project.user.allocation model_project_user_allocation project.group_project_user 1 0 0 0
4 access_project_phase_manager project.phase manager model_project_phase project.group_project_manager 1 1 1 1
5 access_project_user_allocation_manager project.user.allocation manager model_project_user_allocation project.group_project_manager 1 1 1 1
6 access_resource_resource_user user.user user resource.model_resource_resource project.group_project_user 1 0 0 0
7 access_resource_resource_manager user.user manager resource.model_resource_resource project.group_project_manager 1 1 1 1
8 access_project_user_allocation_manager project.user.allocation.manager model_project_user_allocation project.group_project_manager 1 1 1 1
9 access_project_resource_calendar_attendance resource.calendar.attendance resource.model_resource_calendar_attendance project.group_project_manager 1 0 0 0

View File

@ -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"

View File

@ -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"

View File

@ -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:

View File

@ -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>

View File

@ -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:

View File

@ -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>

View File

@ -506,7 +506,7 @@ class purchase_order(osv.osv):
if not acc_id:
acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
if not acc_id:
raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
raise osv.except_osv(_('Error!'), _('Define an expense account for this product: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
else:
acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
fpos = po_line.order_id.fiscal_position or False

View File

@ -44,12 +44,19 @@
attrs="{'invisible': ['|', ('state', 'not in', ('open','done')), ('exclusive', '=', 'exclusive')]}" groups="purchase.group_advance_bidding"/>
</div>
<div class="oe_edit_only">
<label for="name" class="oe_inline"/>
<div class="pull-left">
<label for="name" class="oe_edit_only oe_inline"/>
<h1>
<field name="name" class="oe_inline" attrs="{'readonly': [('state','not in',('draft'))]}"/>
</h1>
</div>
<div class="pull-left">
<label for="origin" class="oe_edit_only oe_inline"/>
<h1>
<label string="," attrs="{'invisible':[('origin','=',False)]}"/>
<field name="origin" class="oe_inline" placeholder="e.g. PO0025"/>
</h1>
</div>
<h1>
<field name="name" class="oe_inline" attrs="{'readonly': [('state','not in',('draft'))]}"/>
</h1>
<group>
<group>
<field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'purchase_requisition.group_purchase_requisition_user', 'purchase.group_purchase_user', 'account.group_account_invoice']}" attrs="{'readonly': [('state','not in',('draft'))]}"/>

View File

@ -36,7 +36,7 @@ class purchase_requisition_partner(osv.osv_memory):
record_id = context and context.get('active_id', False) or False
tender = self.pool.get('purchase.requisition').browse(cr, uid, record_id, context=context)
if not tender.line_ids:
raise osv.except_osv(_('Error!'), _('No product in call for bids.'))
raise osv.except_osv(_('Error!'), _('Define product(s) you want to include in the call for bids.'))
return res
def create_order(self, cr, uid, ids, context=None):

View File

@ -19,7 +19,9 @@
#
##############################################################################
import logging
import openerp
import openerp.tests
_logger = logging.getLogger(__name__)

View File

@ -216,7 +216,7 @@
</group>
<group>
<field name="calendar_id"/>
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
<field name="company_id" widget="selection" groups="base.group_multi_company" attrs="{'invisible':[('calendar_id','=',False)]}"/>
</group>
</group>
<group>

View File

@ -1305,11 +1305,11 @@
<field name="model">stock.incoterms</field>
<field name="arch" type="xml">
<form string="Incoterms" version="7.0">
<group>
<sheet>
<field name="name"/>
<field name="code"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>

View File

@ -34,6 +34,27 @@ import uuid
_logger = logging.getLogger(__name__)
class survey_stage(osv.Model):
"""Stages for Kanban view of surveys"""
_name = 'survey.stage'
_description = 'Survey Stage'
_order = 'sequence asc'
_columns = {
'name': fields.char(string="Name", required=True, translate=True),
'sequence': fields.integer(string="Sequence"),
'closed': fields.boolean(string="Closed", help="If closed, people won't be able to answer to surveys in this column."),
'fold': fields.boolean(string="Folded in kanban view")
}
_defaults = {
'sequence': 1,
'closed': False
}
_sql_constraints = [
('positive_sequence', 'CHECK(sequence >= 0)', 'Sequence number MUST be a natural')
]
class survey_survey(osv.Model):
'''Settings for a multi-page/multi-question survey.
@ -175,9 +196,15 @@ class survey_survey(osv.Model):
'quizz_mode': fields.boolean(string='Quizz mode')
}
def _default_stage(self, cr, uid, context=None):
ids = self.pool['survey.stage'].search(cr, uid, [], limit=1, context=context)
if ids:
return ids[0]
return False
_defaults = {
'color': 0,
'stage_id': lambda self, cr, uid, context: self.pool.get('survey.stage').search_read(cr, uid, fields=['id'], order='sequence asc', limit=1, context=context)[0]['id']
'stage_id': lambda self, *a, **kw: self._default_stage(*a, **kw)
}
def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
@ -456,26 +483,6 @@ class survey_survey(osv.Model):
}
class survey_stage(osv.Model):
"""Stages for Kanban view of surveys"""
_name = 'survey.stage'
_description = 'Survey Stage'
_order = 'sequence asc'
_columns = {
'name': fields.char(string="Name", required=True, translate=True),
'sequence': fields.integer(string="Sequence"),
'closed': fields.boolean(string="Closed", help="If closed, people won't be able to answer to surveys in this column."),
'fold': fields.boolean(string="Folded in kanban view")
}
_defaults = {
'sequence': 1,
'closed': False
}
_sql_constraints = [
('positive_sequence', 'CHECK(sequence >= 0)', 'Sequence number MUST be a natural')
]
class survey_page(osv.Model):

View File

@ -130,6 +130,7 @@
*/
var dummy = function () {};
var website = openerp.website;
website.add_template_file('/website/static/src/xml/website.snippets.xml');
@ -361,15 +362,21 @@
},
clean_for_save: function () {
var self = this;
$(website.snippet.globalSelector).each(function () {
var $snippet = $(this);
self.make_active($snippet);
self.make_active(false);
var editor = $snippet.data("snippet-editor");
if (editor) {
editor.clean_for_save();
$("*[contentEditable], *[attributeEditable]")
.removeAttr('contentEditable')
.removeAttr('attributeEditable');
var options = website.snippet.options;
var template = website.snippet.templateOptions;
for (var k in options) {
if (template[k] && options[k].prototype.clean_for_save !== dummy) {
var $snippet = this.dom_filter(template[k].selector);
$snippet.each(function () {
new options[k](self, null, $(this), k).clean_for_save();
});
}
});
}
},
make_active: function ($snippet) {
if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) {
@ -853,8 +860,7 @@
}
},
clean_for_save: function () {
}
clean_for_save: dummy
});
website.snippet.options.background = website.snippet.Option.extend({
@ -942,8 +948,7 @@
this.id = this.unique_id();
this.$target.attr("id", this.id);
this.$target.find("[data-slide]").attr("data-cke-saved-href", "#" + this.id);
this.$target.find("[data-slide-to]").attr("data-target", "#" + this.id);
this.$target.find("[data-target]").attr("data-target", "#" + this.id);
this.rebind_event();
},
on_clone: function ($clone) {
@ -963,7 +968,7 @@
},
clean_for_save: function () {
this._super();
this.$target.find(".item").removeClass("next prev left right");
$(".carousel").find(".item").removeClass("next prev left right active");
if(!this.$target.find(".item.active").length) {
this.$target.find(".item:first").addClass("active");
}
@ -1757,19 +1762,6 @@
}
this.$overlay.removeClass('oe_active');
},
/* clean_for_save
* function called just before save vue
*/
clean_for_save: function () {
for (var i in this.styles){
this.styles[i].clean_for_save();
}
this.$target.removeAttr('contentEditable')
.find('*').removeAttr('contentEditable');
this.$target.removeAttr('attributeEditable')
.find('*').removeAttr('attributeEditable');
},
});
})();

View File

@ -52,7 +52,7 @@ class BlogPost(osv.Model):
'name': fields.char('Title', required=True, translate=True),
'subtitle': fields.char('Sub Title', translate=True),
'author_id': fields.many2one('res.partner', 'Author'),
'background_image': fields.binary('Background Image'),
'background_image': fields.binary('Background Image', oldname='content_image'),
'blog_id': fields.many2one(
'blog.blog', 'Blog',
required=True, ondelete='cascade',

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-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
@ -19,7 +19,5 @@
#
##############################################################################
import project_long_term
import wizard
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
import certification
import controllers

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-Today OpenERP S.A. (<http://www.openerp.com>).
# Copyright (C) 2004-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
@ -19,24 +19,20 @@
#
##############################################################################
{
'name': 'Portal Project Long Term',
'name': 'Certified People',
'category': 'Website',
'summary': 'Display your network of certified people on your website',
'version': '1.0',
'category': 'Tools',
'complexity': 'easy',
'author': 'OpenERP S.A.',
'depends': ['marketing', 'website'],
'description': """
This module adds necessary security rules and access rights for project long term and portal.
=============================================================================================
Display your network of certified people on your website
""",
'author': 'OpenERP SA',
'depends': ['project_long_term', 'portal'],
'data': [
'security/portal_security.xml',
'security/ir.model.access.csv',
'views/website_certification_views.xml',
'views/website_certification_templates.xml',
],
'installable': True,
'auto_install': True,
'category': 'Hidden',
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-s
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-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
@ -19,7 +19,25 @@
#
##############################################################################
import project_compute_phases
import project_compute_tasks
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from openerp.osv import osv, fields
class certification_type(osv.Model):
_name = 'certification.type'
_order = 'name ASC'
_columns = {
'name': fields.char("Certification Type", required=True)
}
class certification_certification(osv.Model):
_name = 'certification.certification'
_order = 'certification_date DESC'
_columns = {
'partner_id': fields.many2one('res.partner', string="Partner", required=True),
'type_id': fields.many2one('certification.type', string="Certification", required=True),
'certification_date': fields.date("Certification Date", required=True),
'certification_score': fields.char("Certification Score", required=True),
'certification_hidden_score': fields.boolean("Hide score on website?")
}

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-Today OpenERP S.A. (<http://www.openerp.com>).
# Copyright (C) 2004-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
@ -18,3 +18,5 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import main

View File

@ -0,0 +1,49 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-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/>.
#
##############################################################################
from openerp.addons.web import http
from openerp.addons.web.http import request
class WebsiteCertifiedPartners(http.Controller):
@http.route(['/certifications',
'/certifications/<model("certification.type"):cert_type>'], type='http', auth='public',
website=True, multilang=True)
def certified_partners(self, cert_type=None, **post):
cr, uid, context = request.cr, request.uid, request.context
certification_obj = request.registry['certification.certification']
cert_type_obj = request.registry['certification.type']
domain = []
if cert_type:
domain.append(('type_id', '=', cert_type.id))
certifications_ids = certification_obj.search(cr, uid, domain, context=context)
certifications = certification_obj.browse(cr, uid, certifications_ids, context=context)
types = cert_type_obj.browse(cr, uid, cert_type_obj.search(cr, uid, [], context=context), context=context)
data = {
'certifications': certifications,
'types': types
}
return request.website.render("website_certification.certified_partners", data)

View File

@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_certifications_public,certification.certification public,model_certification_certification,base.group_public,1,0,0,0
access_certifications_types_public,certification.type public,model_certification_type,base.group_public,1,0,0,0
access_certifications_users,certification.certification users,model_certification_certification,base.group_user,1,0,0,0
access_certifications_types_users,certification.type users,model_certification_type,base.group_user,1,0,0,0
access_certifications_marketing,certification.certification marketing,model_certification_certification,marketing.group_marketing_user,1,1,1,1
access_certifications_types_marketing,certification.type marketing,model_certification_type,marketing.group_marketing_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_certifications_public certification.certification public model_certification_certification base.group_public 1 0 0 0
3 access_certifications_types_public certification.type public model_certification_type base.group_public 1 0 0 0
4 access_certifications_users certification.certification users model_certification_certification base.group_user 1 0 0 0
5 access_certifications_types_users certification.type users model_certification_type base.group_user 1 0 0 0
6 access_certifications_marketing certification.certification marketing model_certification_certification marketing.group_marketing_user 1 1 1 1
7 access_certifications_types_marketing certification.type marketing model_certification_type marketing.group_marketing_user 1 1 1 1

View File

@ -0,0 +1,42 @@
<?xml version='1.0' encoding='utf-8'?>
<openerp>
<data>
<template id="certified_partners" name="Certified People">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="container">
<div class="row">
<h1>Certified People</h1>
<div class="well">Filter by certification type:
<a href="/certifications"><span class="badge">all</span></a>
<t t-foreach="types" t-as="t">
<a t-att-href="'/certifications/%s' % slug(t)"><span class="badge" t-field="t.name" /></a>
</t>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead><tr>
<th>Name</th>
<th>Date</th>
<th>Type</th>
<th>Score</th>
</tr></thead>
<tbody>
<tr t-foreach="certifications" t-as="c">
<td><span t-field="c.partner_id"/></td>
<td><span t-field="c.certification_date"/></td>
<td><span t-field="c.type_id.name"/></td>
<td><t t-if="c.certification_hidden_score == False"><span t-field="c.certification_score"/></t></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="oe_structure"/>
</div>
</t>
</template>
</data>
</openerp>

View File

@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<openerp>
<data>
<record model="ir.ui.view" id="certification_certification_tree">
<field name="name">view.certification.certification.tree</field>
<field name="model">certification.certification</field>
<field name="arch" type="xml">
<tree string="Granted Certifications">
<field name="partner_id" />
<field name="type_id" />
<field name="certification_date" />
<field name="certification_score" />
<field name="certification_hidden_score" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="certification_certification_form">
<field name="name">view.certification.certification.form</field>
<field name="model">certification.certification</field>
<field name="arch" type="xml">
<form string="Granted Certification" version="7.0">
<sheet>
<group nolabel="1">
<field name="partner_id" />
<field name="type_id" />
<field name="certification_date" />
<field name="certification_score" />
<field name="certification_hidden_score" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="certification_certification_search">
<field name="name">view.certification.certification.search</field>
<field name="model">certification.certification</field>
<field name="arch" type="xml">
<search string="Search Certification">
<field name="partner_id" />
<field name="type_id" />
<group expand="0" string="Group By...">
<filter string="Partner" name="group_by_partner" domain="[]" context="{'group_by': 'partner_id'}"/>
<filter string="Type" name="group_by_type" domain="[]" context="{'group_by': 'type_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_certifications_list">
<field name="name">Certifications</field>
<field name="res_model">certification.certification</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="certification_certification_search"/>
</record>
<menuitem name="Certifications" id="menu_certifications" parent="base.marketing_menu" sequence="35" />
<menuitem name="Certifications" id="menu_certifications_list" action="action_certifications_list" parent="menu_certifications" sequence="1"/>
</data>
</openerp>

View File

@ -32,12 +32,21 @@ class WebsiteForum(http.Controller):
return msg
def _prepare_forum_values(self, forum=None, **kwargs):
Forum = request.registry['forum.forum']
user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context)
public_uid = request.registry['website'].get_public_user(request.cr, request.uid, request.context)
values = {'user': user, 'is_public_user': user.id == public_uid,
'notifications': self._get_notifications(),
'header': kwargs.get('header', dict()),
'searches': kwargs.get('searches', dict())}
'searches': kwargs.get('searches', dict()),
'can_edit_own': True,
'can_edit_all': user.karma > Forum._karma_modo_edit_all,
'can_close_own': user.karma > Forum._karma_modo_close_own,
'can_close_all': user.karma > Forum._karma_modo_close_all,
'can_unlink_own': user.karma > Forum._karma_modo_unlink_own,
'can_unlink_all': user.karma > Forum._karma_modo_unlink_all,
'can_unlink_comment': user.karma > Forum._karma_modo_unlink_comment,
}
if forum:
values['forum'] = forum
elif kwargs.get('forum_id'):
@ -45,6 +54,14 @@ class WebsiteForum(http.Controller):
values.update(kwargs)
return values
def _has_enough_karma(self, karma_name, uid=None):
Forum = request.registry['forum.forum']
karma = hasattr(Forum, karma_name) and getattr(Forum, karma_name) or 0
user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, uid or request.uid, context=request.context)
if user.karma < karma:
return False, {'error': 'not_enough_karma', 'karma': karma}
return True, {}
# Forum
# --------------------------------------------------
@ -204,6 +221,10 @@ class WebsiteForum(http.Controller):
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/ask_for_close', type='http', auth="user", multilang=True, website=True)
def question_ask_for_close(self, forum, question, **post):
check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_close_own' or '_karma_modo_close_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
cr, uid, context = request.cr, request.uid, request.context
Reason = request.registry['forum.post.reason']
reason_ids = Reason.search(cr, uid, [], context=context)
@ -211,7 +232,7 @@ class WebsiteForum(http.Controller):
values = self._prepare_forum_values(**post)
values.update({
'post': question,
'question': question,
'question': question,
'forum': forum,
'reasons': reasons,
@ -228,6 +249,10 @@ class WebsiteForum(http.Controller):
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/close', type='http', auth="user", multilang=True, methods=['POST'], website=True)
def question_close(self, forum, question, **post):
check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_close_own' or '_karma_modo_close_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
request.registry['forum.post'].write(request.cr, request.uid, [question.id], {
'state': 'close',
'closed_uid': request.uid,
@ -238,17 +263,28 @@ class WebsiteForum(http.Controller):
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/reopen', type='http', auth="user", multilang=True, website=True)
def question_reopen(self, forum, question, **kwarg):
check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_close_own' or '_karma_modo_close_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'state': 'active'}, context=request.context)
return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/delete', type='http', auth="user", multilang=True, website=True)
def question_delete(self, forum, question, **kwarg):
#instead of unlink record just change 'active' to false so user can undelete it.
check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_unlink_own' or '_karma_modo_unlink_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': False}, context=request.context)
return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/undelete', type='http', auth="user", multilang=True, website=True)
def question_undelete(self, forum, question, **kwarg):
check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_unlink_own' or '_karma_modo_unlink_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': True}, context=request.context)
return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
@ -287,19 +323,27 @@ class WebsiteForum(http.Controller):
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)
def post_toggle_correct(self, forum, post, **kwargs):
cr, uid, context = request.cr, request.uid, request.context
if post.parent_id is False:
return request.redirect('/')
if not request.session.uid:
return {'error': 'anonymous_user'}
# if user have not access to accept answer then reise warning
if post.parent_id is False or post.parent_id.create_uid.id != uid:
user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context)
if post.parent_id.create_uid.id != uid:
return {'error': 'own_post'}
if post.create_uid.id == user.id and user.karma < request.registry['forum.forum']._karma_answer_accept_own:
return {'error': 'not_enough_karma', 'karma': 20}
# set all answers to False, only one can be accepted
request.registry['forum.post'].write(cr, uid, [c.id for c in post.parent_id.child_ids], {'is_correct': False}, context=context)
request.registry['forum.post'].write(cr, uid, [post.id, post.parent_id.id], {'is_correct': not post.is_correct}, context=context)
request.registry['forum.post'].write(cr, uid, [post.id], {'is_correct': not post.is_correct}, context=context)
return not post.is_correct
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/delete', type='http', auth="user", multilang=True, website=True)
def post_delete(self, forum, post, **kwargs):
check_res = self._has_enough_karma(post.create_uid.id == request.uid and '_karma_modo_unlink_own' or '_karma_modo_unlink_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
question = post.parent_id
request.registry['forum.post'].unlink(request.cr, request.uid, [post.id], context=request.context)
if question:
@ -308,6 +352,10 @@ class WebsiteForum(http.Controller):
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/edit', type='http', auth="user", website=True, multilang=True)
def post_edit(self, forum, post, **kwargs):
check_res = self._has_enough_karma(post.create_uid.id == request.uid and '_karma_modo_edit_own' or '_karma_modo_edit_all')
if not check_res[0]:
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
tags = ""
for tag_name in post.tag_ids:
tags += tag_name.name + ","
@ -345,15 +393,15 @@ class WebsiteForum(http.Controller):
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/upvote', type='json', auth="public", multilang=True, website=True)
def post_upvote(self, forum, post, **kwargs):
# check for karma and not self vote
if not request.session.uid:
return {'error': 'anonymous_user'}
if request.uid == post.create_uid.id:
return {'error': 'own_post'}
user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context)
if user.karma <= 5:
return {'error': 'not_enough_karma', 'karma': 1}
return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=True, context=request.context)
check_res = self._has_enough_karma('_karma_upvote')
if not check_res[0]:
return check_res[1]
upvote = True if not post.user_vote > 0 else False
return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=upvote, context=request.context)
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/downvote', type='json', auth="public", multilang=True, website=True)
def post_downvote(self, forum, post, **kwargs):
@ -361,10 +409,11 @@ class WebsiteForum(http.Controller):
return {'error': 'anonymous_user'}
if request.uid == post.create_uid.id:
return {'error': 'own_post'}
user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context)
if user.karma <= 50:
return {'error': 'not_enough_karma', 'karma': 50}
return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=False, context=request.context)
check_res = self._has_enough_karma('_karma_downvote')
if not check_res[0]:
return check_res[1]
upvote = True if post.user_vote < 0 else False
return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=upvote, context=request.context)
# User
# --------------------------------------------------
@ -392,6 +441,16 @@ class WebsiteForum(http.Controller):
return request.website.render("website_forum.users", values)
@http.route(['/forum/<model("forum.forum"):forum>/partner/<int:partner_id>'], type='http', auth="public", website=True, multilang=True)
def open_partner(self, forum, partner_id=0, **post):
cr, uid, context = request.cr, request.uid, request.context
pids = request.registry['res.partner'].search(cr, SUPERUSER_ID, [('id', '=', partner_id)], context=context)
if pids:
partner = request.registry['res.partner'].browse(cr, SUPERUSER_ID, pids[0], context=context)
if partner.user_ids:
return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), partner.user_ids[0].id))
return werkzeug.utils.redirect("/forum/%s" % slug(forum))
@http.route(['/forum/<model("forum.forum"):forum>/user/<int:user_id>'], type='http', auth="public", website=True, multilang=True)
def open_user(self, forum, user_id=0, **post):
cr, uid, context = request.cr, request.uid, request.context

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<data noupdate="1">
<!-- QUALITY (VOTES) -->
<!-- Teacher: at least 3 upvotes -->
@ -8,6 +8,7 @@
<field name="name">Teacher</field>
<field name="description">Received at least 3 upvote for an answer for the first time</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_teacher">
<field name="name">Teacher</field>
@ -42,6 +43,7 @@
<field name="name">Nice Answer</field>
<field name="description">Answer voted up 4 times</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_nice_answer">
<field name="name">Nice Answer (4)</field>
@ -76,6 +78,7 @@
<field name="name">Good Answer</field>
<field name="description">Answer voted up 6 times</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_good_answer">
<field name="name">Good Answer (6)</field>
@ -110,6 +113,7 @@
<field name="name">Great Answer</field>
<field name="description">Answer voted up 15 times</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_great_answer">
<field name="name">Great Answer (15)</field>
@ -146,6 +150,7 @@
<field name="name">Enlightened</field>
<field name="description">Answer was accepted with 3 or more votes</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_enlightened">
<field name="name">Enlightened</field>
@ -180,6 +185,7 @@
<field name="name">Guru</field>
<field name="description">Answer accepted with 15 or more votes</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_guru">
<field name="name">Guru (15)</field>
@ -256,6 +262,7 @@ for post in Post.browse(cr, uid, user_posts, context=context):
<field name="name">Self-Learner</field>
<field name="description">Answered own question with at least 4 up votes</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_self_learner">
<field name="name">Self-Learner</field>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<data noupdate="1">
<!-- Cleanup: answer or question edition -->
<!-- Not rollback feature in forum -->
@ -15,6 +15,7 @@
<field name="name">Critic</field>
<field name="description">First downvote</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_critic">
<field name="name">Critic</field>
@ -50,6 +51,7 @@
<field name="name">Disciplined</field>
<field name="description">Deleted own post with 3 or more upvotes</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_disciplined">
<field name="name">Disciplined</field>
@ -85,6 +87,7 @@
<field name="name">Editor</field>
<field name="description">First edit</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_editor">
<field name="name">Editor</field>
@ -223,6 +226,7 @@ result = int(len(data) >= 15)</field>
<field name="name">Supporter</field>
<field name="description">First upvote</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_supporter">
<field name="name">Supporter</field>
@ -258,6 +262,7 @@ result = int(len(data) >= 15)</field>
<field name="name">Peer Pressure</field>
<field name="description">Deleted own post with 3 or more downvotes</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_peer_pressure">
<field name="name">Peer Pressure</field>

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<data noupdate="1">
<!-- Biography: complet your profile -->
<record id="badge_p_1" model="gamification.badge">
<field name="name">Autobiographer</field>
<field name="description">Completed own biography</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_configure_profile">
<field name="name">Completed own biography</field>
@ -46,6 +47,7 @@
<field name="name">Commentator</field>
<field name="description">Posted 10 comments</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_commentator">
<field name="name">Commentator</field>
@ -81,6 +83,7 @@
<field name="name">Pundit</field>
<field name="description">Left comments with score of 10 or more</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_pundit">
<field name="name">Pundit</field>
@ -115,6 +118,7 @@
<field name="name">Chief Commentator</field>
<field name="description">Posted 100 comments</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.challenge" id="challenge_chief_commentator">
<field name="name">Chief Commentator</field>
@ -177,6 +181,7 @@ result = get_counter(cr, uid, context=context)
<field name="name">Taxonomist</field>
<field name="description">Created a tag used by 15 questions</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_taxonomist">
<field name="name">Taxonomist</field>

View File

@ -1,6 +1,6 @@
<!-- <?xml version="1.0" encoding="utf-8"?> -->
<openerp>
<data>
<data noupdate="1">
<!-- POPULARITY (VIEWS) -->
<!-- Popular: 150 views -->
@ -8,6 +8,7 @@
<field name="name">Popular Question</field>
<field name="description">Asked a question with at least 150 views</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_popular_question">
<field name="name">Popular Question (150)</field>
@ -43,6 +44,7 @@
<field name="name">Notable Question</field>
<field name="description">Asked a question with at least 250 views</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_notable_question">
<field name="name">Popular Question (250)</field>
@ -77,6 +79,7 @@
<field name="name">Famous Question</field>
<field name="description">Asked a question with at least 500 views</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_famous_question">
<field name="name">Popular Question (500)</field>
@ -113,6 +116,7 @@
<field name="name">Credible Question</field>
<field name="description">Question set as favorite by 1 user</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_favorite_question_1">
<field name="name">Favourite Question (1)</field>
@ -147,6 +151,7 @@
<field name="name">Favorite Question</field>
<field name="description">Question set as favorite by 5 users</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_favorite_question_5">
<field name="name">Favourite Question (5)</field>
@ -181,6 +186,7 @@
<field name="name">Stellar Question</field>
<field name="description">Question set as favorite by 25 users</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_stellar_question_25">
<field name="name">Favourite Question (25)</field>
@ -217,6 +223,7 @@
<field name="name">Student</field>
<field name="description">Asked first question with at least one up vote</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_student">
<field name="name">Upvoted question (1)</field>
@ -251,6 +258,7 @@
<field name="name">Nice Quesiotn</field>
<field name="description">Question voted up 4 times</field>
<field name="level">bronze</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_nice_question">
<field name="name">Upvoted question (4)</field>
@ -285,6 +293,7 @@
<field name="name">Good Question</field>
<field name="description">Question voted up 6 times</field>
<field name="level">silver</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_good_question">
<field name="name">Upvoted question (6)</field>
@ -319,6 +328,7 @@
<field name="name">Great Question</field>
<field name="description">Question voted up 15 times</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_great_question">
<field name="name">Upvoted question (15)</field>
@ -354,6 +364,7 @@
<field name="name">Scholar</field>
<field name="description">Asked a question and accepted an answer</field>
<field name="level">gold</field>
<field name="rule_auth">nobody</field>
</record>
<record model="gamification.goal.definition" id="definition_scholar">
<field name="name">Scholar</field>

View File

@ -9,9 +9,38 @@ from openerp.tools.translate import _
class Forum(osv.Model):
"""TDE TODO: set karma values for actions dynamic for a given forum"""
_name = 'forum.forum'
_description = 'Forums'
_inherit = ['website.seo.metadata']
# karma values
_karma_upvote = 5 # done
_karma_downvote = 50 # done
_karma_answer_accept_own = 20 # done
_karma_answer_accept_own_now = 50
_karma_answer_accept_all = 500
_karma_editor_link_files = 30 # done
_karma_editor_clickable_link = 50
_karma_comment = 1
_karma_modo_retag = 75
_karma_modo_flag = 100
_karma_modo_flag_see_all = 300
_karma_modo_unlink_comment = 750
_karma_modo_edit_own = 1 # done
_karma_modo_edit_all = 300 # done
_karma_modo_close_own = 100 # done
_karma_modo_close_all = 900 # done
_karma_modo_unlink_own = 500 # done
_karma_modo_unlink_all = 1000 # done
# karma generation
_karma_gen_quest_new = 2 # done
_karma_gen_upvote_quest = 5 # done
_karma_gen_downvote_quest = -2 # done
_karma_gen_upvote_ans = 10 # done
_karma_gen_downvote_ans = -2 # done
_karma_gen_ans_accept = 2 # done
_karma_gen_ans_accepted = 15 # done
_karma_gen_ans_flagged = -100
_columns = {
'name': fields.char('Name', required=True, translate=True),
@ -97,6 +126,13 @@ class Post(osv.Model):
res[post.id] = any(answer.create_uid.id == uid for answer in post.child_ids)
return res
def _get_has_validated_answer(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, False)
ans_ids = self.search(cr, uid, [('parent_id', 'in', ids), ('is_correct', '=', True)], context=context)
for answer in self.browse(cr, uid, ans_ids, context=context):
res[answer.parent_id.id] = True
return res
def _is_self_reply(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, False)
for post in self.browse(cr, uid, ids, context=context):
@ -143,7 +179,8 @@ class Post(osv.Model):
}),
# hierarchy
'parent_id': fields.many2one('forum.post', 'Question', ondelete='cascade'),
'self_reply': fields.function(_is_self_reply, 'Reply to own question', type='boolean',
'self_reply': fields.function(
_is_self_reply, 'Reply to own question', type='boolean',
store={
'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['parent_id', 'create_uid'], 10),
}),
@ -156,6 +193,12 @@ class Post(osv.Model):
'uid_has_answered': fields.function(
_get_uid_answered, string='Has Answered', type='boolean',
),
'has_validated_answer': fields.function(
_get_has_validated_answer, string='Has a Validated Answered', type='boolean',
store={
'forum.post': (_get_post_from_hierarchy, ['parent_id', 'child_ids', 'is_correct'], 10),
}
),
# closing
'closed_reason_id': fields.many2one('forum.post.reason', 'Reason'),
'closed_uid': fields.many2one('res.users', 'Closed by', select=1),
@ -183,10 +226,18 @@ class Post(osv.Model):
self.message_post(cr, uid, parent.id, subject=_('Re: %s') % parent.name, body=body, subtype='website_forum.mt_answer_new', context=context)
else:
self.message_post(cr, uid, post_id, subject=vals.get('name', ''), body=_('New Question Created'), subtype='website_forum.mt_question_new', context=context)
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], 2, context=context)
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], self.pool['forum.forum']._karma_gen_quest_new, context=context)
return post_id
def write(self, cr, uid, ids, vals, context=None):
Forum = self.pool['forum.forum']
# update karma when accepting/rejecting answers
if 'is_correct' in vals:
mult = 1 if vals['is_correct'] else -1
for post in self.browse(cr, uid, ids, context=context):
if vals['is_correct'] != post.is_correct:
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], Forum._karma_gen_ans_accepted * mult, context=context)
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], Forum._karma_gen_ans_accept * mult, context=context)
res = super(Post, self).write(cr, uid, ids, vals, context=context)
# if post content modify, notify followers
if 'content' in vals or 'name' in vals:
@ -198,11 +249,6 @@ class Post(osv.Model):
body, subtype = _('Question Edited'), 'website_forum.mt_question_edit'
obj_id = post.id
self.message_post(cr, uid, obj_id, body=_(body), subtype=subtype, context=context)
# update karma of related user when any answer accepted
if 'correct' in vals:
for post in self.browse(cr, uid, ids, context=context):
karma_value = 15 if vals.get('correct') else -15
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], {'karma': karma_value}, context=context)
return res
def vote(self, cr, uid, ids, upvote=True, context=None):
@ -252,16 +298,30 @@ class Vote(osv.Model):
def create(self, cr, uid, vals, context=None):
vote_id = super(Vote, self).create(cr, uid, vals, context=context)
karma_value = int(vals.get('vote', '1')) * 10
post = self.pool['forum.post'].browse(cr, uid, vals.get('post_id'), context=context)
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, post.create_uid.id, karma_value, context=context)
if vals.get('vote', '1') == '1':
karma = self.pool['forum.forum']._karma_upvote
elif vals.get('vote', '1') == '-1':
karma = self.pool['forum.forum']._karma_downvote
post = self.pool['forum.post'].browse(cr, uid, vals['post_id'], context=context)
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], karma, context=context)
return vote_id
def write(self, cr, uid, ids, values, context=None):
def _get_karma_value(old_vote, new_vote, up_karma, down_karma):
_karma_upd = {
'-1': {'-1': 0, '0': -1 * down_karma, '1': -1 * down_karma + up_karma},
'0': {'-1': 1 * down_karma, '0': 0, '1': up_karma},
'1': {'-1': -1 * up_karma + down_karma, '0': -1 * up_karma, '1': 0}
}
return _karma_upd[old_vote][new_vote]
if 'vote' in values:
Forum = self.pool['forum.forum']
for vote in self.browse(cr, uid, ids, context=context):
karma_value = (int(values.get('vote')) - int(vote.vote)) * 10
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, vote.post_id.create_uid.id, karma_value, context=context)
if vote.post_id.parent_id:
karma_value = _get_karma_value(vote.vote, values['vote'], Forum._karma_gen_upvote_ans, Forum._karma_gen_downvote_ans)
else:
karma_value = _get_karma_value(vote.vote, values['vote'], Forum._karma_gen_upvote_quest, Forum._karma_gen_downvote_quest)
self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.post_id.create_uid.id], karma_value, context=context)
res = super(Vote, self).write(cr, uid, ids, values, context=context)
return res

View File

@ -32,8 +32,6 @@ class Users(osv.Model):
}
def add_karma(self, cr, uid, ids, karma, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
for user in self.browse(cr, uid, ids, context=context):
self.write(cr, uid, [user.id], {'karma': user.karma + karma}, context=context)
return True
@ -44,4 +42,4 @@ class Users(osv.Model):
excluded_categories.append('forum')
else:
excluded_categories = ['forum']
return super(Users, self).get_serialised_gamification_summary(cr, uid, excluded_categories=excluded_categories, context=context)
return super(Users, self).get_serialised_gamification_summary(cr, uid, excluded_categories=excluded_categories, context=context)

View File

@ -18,9 +18,9 @@ $(document).ready(function () {
'</div>');
}
else if (data['error'] == 'not_enough_karma') {
var $warning = $('<div class="alert alert-danger alert-dismissable" id="vote_alert" style="position:absolute; margin-top: -30px; margin-left: 90px;">'+
var $warning = $('<div class="alert alert-danger alert-dismissable" id="vote_alert" style="max-width: 500px; position:absolute; margin-top: -30px; margin-left: 90px;">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
'Sorry, at least ' + data['karma'] + ' karma is required to vote'+
'Sorry, at least ' + data['karma'] + ' karma is required to vote. You can gain karma by answering questions and receiving votes.'+
'</div>');
}
vote_alert = $link.parent().find("#vote_alert");
@ -57,7 +57,12 @@ $(document).ready(function () {
} else if (data['error'] == 'own_post'){
var $warning = $('<div class="alert alert-danger alert-dismissable" id="correct_answer_alert" style="position:absolute; margin-top: -30px; margin-left: 90px;">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
'Sorry, the user who asked this question can only accept the answer as correct.'+
'Sorry, only the user who asked this question can accept the answer as correct.'+
'</div>');
} else if (data['error'] == 'not_enough_karma') {
var $warning = $('<div class="alert alert-danger alert-dismissable" id="vote_alert" style="max-width: 500px; position:absolute; margin-top: -30px; margin-left: 90px;">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
'Sorry, at least ' + data['karma'] + ' karma is required to accept your own answers. You can gain karma by answering questions and receiving votes.'+
'</div>');
}
correct_answer_alert = $link.parent().find("#correct_answer_alert");

View File

@ -333,7 +333,7 @@
them.
</p>
<form t-attf-action="/forum/#{ slug(forum) }/question/#{slug(post)}/close" method="post" role="form" class="form-horizontal mt32 mb64">
<input name="post_id" t-att-value="post.id" type="hidden"/>
<input name="post_id" t-att-value="question.id" type="hidden"/>
<div class="form-group">
<label class="col-md-3 control-label" for="reason">Question:</label>
<div class="col-md-8 mt8">
@ -345,7 +345,7 @@
<div class="col-md-8">
<select class="form-control" name="reason_id">
<t t-foreach="reasons or []" t-as="reason">
<option t-att-value="reason.id" t-att-selected="reason.id == post.closed_reason_id.id"><t t-esc="reason.name"/></option>
<option t-att-value="reason.id" t-att-selected="reason.id == question.closed_reason_id.id"><t t-esc="reason.name"/></option>
</t>
</select>
</div>
@ -447,16 +447,19 @@
Comment
</a>
</li>
<li t-if="question.state != 'close' and (user.id == question.create_uid.id or user.karma&gt;=100)">
<li t-if="question.state != 'close' and ((user.id == question.create_uid.id and can_close_own) or can_close_all)">
<a class="text-muted fa fa-times" t-attf-href="/forum/#{ slug(forum) }/question/#{slug(question)}/ask_for_close"> Close</a>
</li>
<li t-if="user.id == question.create_uid.id or user.karma&gt;=300">
<li t-if="question.state == 'close' and ((user.id == question.create_uid.id and can_close_own) or can_close_all)">
<a class="text-muted fa fa-undo" t-attf-href="/forum/#{ slug(forum) }/question/#{slug(question)/reopen"> Reopen</a>
</li>
<li t-if="(user.id == question.create_uid.id and can_edit_own) or can_edit_all">
<a class="text-muted fa fa-edit" t-attf-href="/forum/#{ slug(forum) }/post/#{slug(question)}/edit"> Edit</a>
</li>
<li t-if="question.active and user.id == question.create_uid.id or user.karma&gt;=1000">
<li t-if="question.active and ((user.id == question.create_uid.id and can_unlink_own) or can_unlink_all)">
<a class="text-muted fa fa-trash-o" t-attf-href="/forum/#{ slug(forum) }/question/#{slug(question)}/delete"> Delete</a>
</li>
<li t-if="uid == question.create_uid.id and not question.active">
<li t-if="not question.active and ((user.id == question.create_uid.id and can_unlink_own) or can_unlink_all)">
<a class="text-muted fa fa-trash-o" t-attf-href="/forum/#{ slug(forum) }/question/#{slug(question)}/undelete"> Undelete</a>
</li>
</ul>
@ -500,10 +503,10 @@
t-attf-data-target="#comment#{ answer._name.replace('.','') + '-' + str(answer.id) }"> Comment
</a>
</li>
<li t-if="user.id == answer.create_uid.id or user.karma&gt;=300">
<li t-if="(user.id == answer.create_uid.id and can_edit_own) or can_edit_all">
<a class="text-muted fa fa-edit" t-attf-href="/forum/#{slug(forum)}/post/#{slug(answer)}/edit"> Edit</a>
</li>
<li t-if="user.id == answer.create_uid.id or user.karma&gt;=1000">
<li t-if="(user.id == answer.create_uid.id and can_unlink_own) or can_unlink_all">
<a class="text-muted fa fa-trash-o" t-attf-href="/forum/#{slug(forum)}/post/#{slug(answer)}/delete"> Delete</a>
</li>
<li t-if="user.id == answer.create_uid.id">
@ -546,7 +549,7 @@
t-attf-href="/forum/#{slug(forum)}/post/#{slug(object)}/comment/#{slug(message)}/delete"
class="close comment_delete">&amp;times;</button>
<span t-field="message.body"/>
<a t-attf-href="/forum/#{slug(forum)}/user/#{message.id}"
<a t-attf-href="/forum/#{slug(forum)}/partner/#{message.author_id.id}"
t-field="message.author_id" t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}'
style="display: inline-block;"/>
on <span t-field="message.date" t-field-options='{"format":"short"}'/>

View File

@ -21,22 +21,6 @@
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.osv import osv
class Website(osv.Model):
_inherit = "website"
def preprocess_request(self, cr, uid, ids, request, context=None):
project_obj = request.registry['project.project']
project_ids = project_obj.search(cr, uid, [('privacy_visibility', "=", "public")], context=request.context)
request.context.update({
'website_project_ids': project_obj.browse(cr, uid, project_ids, context=request.context)
})
return super(Website, self).preprocess_request(cr, uid, ids, request, context=None)
class website_project(http.Controller):

View File

@ -2,15 +2,6 @@
<openerp>
<data>
<!-- Layout add nav and footer -->
<template id="footer_custom" inherit_id="website.layout" name="Footer Project's Links">
<xpath expr="//footer//ul[@name='products']" position="inside">
<li t-foreach="website_project_ids" t-as="project">
<a t-attf-href="/project/#{ project.id }"><span t-field="project.name"/></a>
</li>
</xpath>
</template>
<!-- Project -->
<template id="task_kanban_card" name="TaskKanban">
<div class="thumbnail">