\n"
+" Click to define a new customer segmentation.\n"
+"
\n"
+" Create specific categories which you can assign to your\n"
+" contacts to better manage your interactions with them. The\n"
+" segmentation tool is able to assign categories to contacts\n"
+" according to criteria you set.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: crm
+#: field:crm.opportunity2phonecall,contact_name:0
+#: field:crm.phonecall,partner_id:0
+#: field:crm.phonecall2phonecall,contact_name:0
+msgid "Contact"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.section,change_responsible:0
+msgid ""
+"When escalating to this team override the salesman with the team leader."
+msgstr ""
+
+#. module: crm
+#: model:process.transition,name:crm.process_transition_opportunitymeeting0
+msgid "Opportunity Meeting"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead.report,delay_close:0
+#: help:crm.phonecall.report,delay_close:0
+msgid "Number of Days to close the case"
+msgstr ""
+
+#. module: crm
+#: model:process.node,note:crm.process_node_opportunities0
+msgid "When a real project/opportunity is detected"
+msgstr ""
+
+#. module: crm
+#: field:res.partner,opportunity_ids:0
+msgid "Leads and Opportunities"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.relate_partner_opportunities
+msgid ""
+"
\n"
+" Click to create an opportunity related to this customer.\n"
+"
\n"
+" Use opportunities to keep track of your sales pipeline, "
+"follow\n"
+" up potential sales and better forecast your future "
+"revenues.\n"
+"
\n"
+" You will be able to plan meetings and phone calls from\n"
+" opportunities, convert them into quotations, attach related\n"
+" documents, track all discussions, and much more.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: crm
+#: model:crm.case.stage,name:crm.stage_lead7
+#: view:crm.lead:0
+msgid "Dead"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,message_unread:0
+#: view:crm.lead:0
+#: field:crm.lead,message_unread:0
+#: field:crm.phonecall,message_unread:0
+msgid "Unread Messages"
+msgstr ""
+
+#. module: crm
+#: view:crm.segmentation:0
+#: field:crm.segmentation.line,segmentation_id:0
+#: model:ir.actions.act_window,name:crm.crm_segmentation-act
+msgid "Segmentation"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead2opportunity.partner,action:0
+#: selection:crm.lead2opportunity.partner.mass,action:0
+#: selection:crm.partner.binding,action:0
+msgid "Link to an existing customer"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,write_date:0
+msgid "Update Date"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,user_id:0
+msgid "Team Leader"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,probability:0
+msgid ""
+"This percentage depicts the default/average probability of the Case for this "
+"stage to be a success"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: field:crm.opportunity2phonecall,categ_id:0
+#: field:crm.phonecall,categ_id:0
+#: view:crm.phonecall.report:0
+#: field:crm.phonecall.report,categ_id:0
+#: field:crm.phonecall2phonecall,categ_id:0
+msgid "Category"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "#Opportunities"
+msgstr ""
+
+#. module: crm
+#: code:addons/crm/crm_lead.py:640
+#, python-format
+msgid ""
+"Please select more than one element (lead or opportunity) from the list view."
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Leads that are assigned to one of the sale teams I manage, or to me"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,partner_address_email:0
+msgid "Partner Contact Email"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.crm_case_section_act
+msgid ""
+"
\n"
+" Click to define a new sales team.\n"
+"
\n"
+" Use sales team to organize your different salespersons or\n"
+" departments into separate teams. Each team will work in\n"
+" its own list of opportunities.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: crm
+#: model:process.transition,note:crm.process_transition_opportunitymeeting0
+msgid "Normal or phone meeting for opportunity"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,state:0
+#: field:crm.lead.report,state:0
+#: field:crm.phonecall,state:0
+#: view:crm.phonecall.report:0
+#: field:crm.phonecall.report,state:0
+msgid "Status"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead2opportunity.partner:0
+msgid "Create Opportunity"
+msgstr ""
+
+#. module: crm
+#: view:sale.config.settings:0
+msgid "Configure"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Escalate"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Mailings"
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,description:crm.mt_lead_stage
+msgid "Stage changed"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead.report,creation_month:0
+#: selection:crm.lead.report,deadline_month:0
+#: selection:crm.phonecall.report,month:0
+msgid "June"
+msgstr ""
+
+#. module: crm
+#: selection:crm.segmentation,state:0
+msgid "Not Running"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,planned_revenue:0
+msgid "Planned Revenue"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,planned_revenue:0
+msgid "Expected Revenue"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead.report,creation_month:0
+#: selection:crm.lead.report,deadline_month:0
+#: selection:crm.phonecall.report,month:0
+msgid "October"
+msgstr ""
+
+#. module: crm
+#: view:crm.segmentation:0
+msgid "Included Answers :"
+msgstr ""
+
+#. module: crm
+#: help:crm.phonecall,state:0
+msgid ""
+"The status is set to 'Todo', when a case is created. "
+" If the case is in progress the status is set to 'Open'. "
+" When the call is over, the status is set to 'Held'. "
+" If the call needs to be done then the status is set "
+"to 'Not Held'."
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,message_summary:0
+#: field:crm.lead,message_summary:0
+#: field:crm.phonecall,message_summary:0
+msgid "Summary"
+msgstr ""
+
+#. module: crm
+#: view:crm.merge.opportunity:0
+msgid "Merge"
+msgstr ""
+
+#. module: crm
+#: model:email.template,subject:crm.email_template_opportunity_mail
+msgid "Opportunity ${object.name | h})"
+msgstr ""
+
+#. module: crm
+#: view:crm.case.categ:0
+msgid "Case Category"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,partner_address_name:0
+msgid "Partner Contact Name"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.server,subject:crm.action_email_reminder_lead
+msgid ""
+"Reminder on Lead: [[object.id ]] [[object.partner_id and 'of ' "
+"+object.partner_id.name or '']]"
+msgstr ""
+
+#. module: crm
+#: view:crm.segmentation:0
+msgid "Profiling Options"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+msgid "#Phone calls"
+msgstr ""
+
+#. module: crm
+#: sql_constraint:crm.case.section:0
+msgid "The code of the sales team must be unique !"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,email_from:0
+msgid "Email address of the contact"
+msgstr ""
+
+#. module: crm
+#: selection:crm.case.stage,state:0
+#: view:crm.lead:0
+#: selection:crm.lead,state:0
+msgid "In Progress"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.crm_phonecall_categ_action
+msgid ""
+"
\n"
+" Click to add a new category.\n"
+"
\n"
+" Create specific phone call categories to better define the type "
+"of\n"
+" calls tracked in the system.\n"
+"
\n"
+" OpenERP helps you keep track of your sales pipeline to follow\n"
+" up potential sales and better forecast your future revenues.\n"
+"
\n"
+" You will be able to plan meetings and phone calls from\n"
+" opportunities, convert them into quotations, attach related\n"
+" documents, track all discussions, and much more.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: crm
+#: field:crm.segmentation,partner_id:0
+msgid "Max Partner ID processed"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,on_change:0
+msgid ""
+"Setting this stage will change the probability automatically on the "
+"opportunity."
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "oe_kanban_text_red"
+msgstr ""
+
+#. module: crm
+#: model:ir.ui.menu,name:crm.menu_crm_payment_mode_act
+msgid "Payment Modes"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,opening_date:0
+#: field:crm.phonecall.report,opening_date:0
+msgid "Opening Date"
+msgstr ""
+
+#. module: crm
+#: help:crm.phonecall,duration:0
+msgid "Duration in Minutes"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.channel,name:0
+msgid "Channel Name"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead.report,deadline_day:0
+msgid "Expected closing day"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.section,active:0
+msgid ""
+"If the active field is set to true, it will allow you to hide the sales team "
+"without removing it."
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,case_default:0
+msgid ""
+"If you check this field, this stage will be proposed by default on each "
+"sales team. It will not assign this stage to existing teams."
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,type:0
+msgid ""
+"This field is used to distinguish stages related to Leads from stages "
+"related to Opportunities, or to specify stages available for both types."
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,name:crm.mt_lead_create
+#: model:mail.message.subtype,name:crm.mt_salesteam_lead
+msgid "Lead Created"
+msgstr ""
+
+#. module: crm
+#: help:crm.segmentation,sales_purchase_active:0
+msgid ""
+"Check if you want to use this tab as part of the segmentation rule. If not "
+"checked, the criteria beneath will be ignored"
+msgstr ""
+
+#. module: crm
+#: field:crm.segmentation,state:0
+msgid "Execution Status"
+msgstr ""
+
+#. module: crm
+#: view:crm.opportunity2phonecall:0
+msgid "Log call"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,day_close:0
+msgid "Days to Close"
+msgstr ""
+
+#. module: crm
+#: code:addons/crm/crm_lead.py:1057
+#: field:crm.case.section,complete_name:0
+#, python-format
+msgid "unknown"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,message_is_follower:0
+#: field:crm.lead,message_is_follower:0
+#: field:crm.phonecall,message_is_follower:0
+msgid "Is a Follower"
+msgstr ""
+
+#. module: crm
+#: field:crm.opportunity2phonecall,date:0
+#: view:crm.phonecall:0
+#: field:crm.phonecall,date:0
+#: field:crm.phonecall2phonecall,date:0
+msgid "Date"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.section,name:crm.crm_case_section_4
+msgid "Online Support"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+#: view:crm.phonecall.report:0
+msgid "Extended Filters..."
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+msgid "Phone calls which are in closed state"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+msgid "Search"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,state:0
+msgid ""
+"The Status is set to 'Draft', when a case is created. If the case is in "
+"progress the Status is set to 'Open'. When the case is over, the Status is "
+"set to 'Done'. If the case needs to be reviewed then the Status is set to "
+"'Pending'."
+msgstr ""
+
+#. module: crm
+#: model:crm.case.section,name:crm.crm_case_section_1
+msgid "Sales Marketing Department"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+msgid "Date of call"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,section_id:0
+msgid ""
+"When sending mails, the default email address is taken from the sales team."
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+msgid ""
+"Phone Calls Assigned to the current user or with a team having the current "
+"user as team leader"
+msgstr ""
+
+#. module: crm
+#: view:crm.segmentation:0
+msgid "Segmentation Description"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Lead Description"
+msgstr ""
+
+#. module: crm
+#: code:addons/crm/crm_lead.py:581
+#, python-format
+msgid "Merged opportunities"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor7
+msgid "Consulting"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,code:0
+msgid "Code"
+msgstr ""
+
+#. module: crm
+#: view:sale.config.settings:0
+msgid "Features"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,child_ids:0
+msgid "Child Teams"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+msgid "Phone calls which are in draft and open state"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead2opportunity.partner.mass,user_ids:0
+msgid "Salesmen"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "References"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead2opportunity.partner:0
+#: view:crm.lead2opportunity.partner.mass:0
+#: view:crm.merge.opportunity:0
+#: view:crm.opportunity2phonecall:0
+#: view:crm.phonecall:0
+#: view:crm.phonecall2phonecall:0
+msgid "Cancel"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Opportunities Assigned to Me or My Team(s)"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor4
+msgid "Information"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "Leads/Opportunities which are in pending state"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+msgid "To Do"
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,description:crm.mt_lead_lost
+msgid "Opportunity lost"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead2opportunity.partner,action:0
+#: field:crm.lead2opportunity.partner.mass,action:0
+#: field:crm.partner.binding,action:0
+msgid "Related Customer"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor8
+msgid "Other"
+msgstr ""
+
+#. module: crm
+#: field:crm.phonecall,opportunity_id:0
+#: model:ir.model,name:crm.model_crm_lead
+msgid "Lead/Opportunity"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.action_merge_opportunities
+#: model:ir.actions.act_window,name:crm.merge_opportunity_act
+msgid "Merge leads/opportunities"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,sequence:0
+msgid "Used to order stages. Lower is better."
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.crm_phonecall_categ_action
+msgid "Phonecall Categories"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "Leads/Opportunities which are in open state"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_res_users
+msgid "Users"
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,name:crm.mt_lead_stage
+msgid "Stage Changed"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.stage,section_ids:0
+msgid "Sections"
+msgstr ""
+
+#. module: crm
+#: constraint:crm.case.section:0
+msgid "Error ! You cannot create recursive Sales team."
+msgstr ""
+
+#. module: crm
+#: selection:crm.opportunity2phonecall,action:0
+#: selection:crm.phonecall2phonecall,action:0
+msgid "Log a call"
+msgstr ""
+
+#. module: crm
+#: selection:crm.segmentation.line,expr_name:0
+msgid "Sale Amount"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+#: model:ir.actions.act_window,name:crm.act_crm_opportunity_crm_phonecall_new
+msgid "Phone calls"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.action_report_crm_opportunity
+msgid ""
+"Opportunities Analysis gives you an instant access to your opportunities "
+"with information such as the expected revenue, planned cost, missed "
+"deadlines or the number of interactions per opportunity. This report is "
+"mainly used by the sales manager in order to do the periodic review with the "
+"teams of the sales pipeline."
+msgstr ""
+
+#. module: crm
+#: field:crm.case.categ,name:0
+#: field:crm.payment.mode,name:0
+#: field:crm.segmentation,name:0
+msgid "Name"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "Leads/Opportunities that are assigned to me"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,date_closed:0
+#: field:crm.phonecall.report,date_closed:0
+msgid "Close Date"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "My Case(s)"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.section,message_ids:0
+#: help:crm.lead,message_ids:0
+#: help:crm.phonecall,message_ids:0
+msgid "Messages and communication history"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Show Countries"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: view:crm.phonecall:0
+msgid "Creation"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead,priority:0
+#: selection:crm.lead.report,priority:0
+#: selection:crm.phonecall,priority:0
+#: selection:crm.phonecall.report,priority:0
+msgid "High"
+msgstr ""
+
+#. module: crm
+#: model:process.node,note:crm.process_node_partner0
+msgid "Convert to prospect to business partner"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_crm_payment_mode
+msgid "CRM Payment Mode"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "Leads/Opportunities which are in done state"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,delay_close:0
+msgid "Delay to Close"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: view:crm.lead.report:0
+#: view:crm.phonecall:0
+#: view:crm.phonecall.report:0
+msgid "Group By..."
+msgstr ""
+
+#. module: crm
+#: view:crm.merge.opportunity:0
+msgid "Merge Leads/Opportunities"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,parent_id:0
+msgid "Parent Team"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead2opportunity.partner,action:0
+#: selection:crm.lead2opportunity.partner.mass,action:0
+#: selection:crm.partner.binding,action:0
+msgid "Do not link to a customer"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,date_action:0
+msgid "Next Action Date"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,state:0
+msgid ""
+"The status of your document will automatically change regarding the selected "
+"stage. For example, if a stage is related to the status 'Close', when your "
+"document reaches this stage, it is automatically closed."
+msgstr ""
+
+#. module: crm
+#: view:crm.lead2opportunity.partner.mass:0
+msgid "Assign opportunities to"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_phone1
+msgid "Inbound"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+msgid "Month of call"
+msgstr ""
+
+#. module: crm
+#: code:addons/crm/crm_phonecall.py:290
+#, python-format
+msgid "Partner has been created."
+msgstr ""
+
+#. module: crm
+#: field:sale.config.settings,module_crm_claim:0
+msgid "Manage Customer Claims"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.action_report_crm_lead
+msgid ""
+"Leads Analysis allows you to check different CRM related information like "
+"the treatment delays or number of leads per state. You can sort out your "
+"leads analysis by different groups to get accurate grained analysis."
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor3
+msgid "Services"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead,priority:0
+#: selection:crm.lead.report,priority:0
+#: selection:crm.phonecall,priority:0
+#: selection:crm.phonecall.report,priority:0
+msgid "Highest"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead.report,creation_year:0
+msgid "Creation year"
+msgstr ""
+
+#. module: crm
+#: view:crm.case.section:0
+#: field:crm.lead,description:0
+msgid "Notes"
+msgstr ""
+
+#. module: crm
+#: field:crm.segmentation.line,expr_value:0
+msgid "Value"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,partner_name:0
+msgid "Customer Name"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,reply_to:0
+msgid "Reply-To"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Display"
+msgstr ""
+
+#. module: crm
+#: view:board.board:0
+msgid "Opportunities by Stage"
+msgstr ""
+
+#. module: crm
+#: model:process.transition,note:crm.process_transition_leadpartner0
+msgid "Prospect is converting to business partner"
+msgstr ""
+
+#. module: crm
+#: view:crm.case.channel:0
+#: model:ir.actions.act_window,name:crm.crm_case_channel_action
+#: model:ir.model,name:crm.model_crm_case_channel
+#: model:ir.ui.menu,name:crm.menu_crm_case_channel
+msgid "Channels"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+#: selection:crm.phonecall,state:0
+#: view:crm.phonecall.report:0
+#: selection:crm.phonecall.report,state:0
+msgid "Held"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Extra Info"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Fund Raising"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Edit..."
+msgstr ""
+
+#. module: crm
+#: model:crm.case.resource.type,name:crm.type_lead5
+msgid "Google Adwords"
+msgstr ""
+
+#. module: crm
+#: view:crm.case.section:0
+msgid "Select Stages for this Sales Team"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: field:crm.lead,priority:0
+#: view:crm.lead.report:0
+#: field:crm.lead.report,priority:0
+#: field:crm.phonecall,priority:0
+#: view:crm.phonecall.report:0
+#: field:crm.phonecall.report,priority:0
+msgid "Priority"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_crm_lead2opportunity_partner
+msgid "Lead To Opportunity Partner"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,partner_id:0
+msgid "Linked partner (optional). Usually created when converting the lead."
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,payment_mode:0
+#: view:crm.payment.mode:0
+#: model:ir.actions.act_window,name:crm.action_crm_payment_mode
+msgid "Payment Mode"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_crm_lead2opportunity_partner_mass
+msgid "Mass Lead To Opportunity Partner"
+msgstr ""
+
+#. module: crm
+#: view:sale.config.settings:0
+msgid "On Mail Server"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.open_board_statistical_dash
+#: model:ir.ui.menu,name:crm.menu_board_statistics_dash
+msgid "CRM"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.crm_segmentation_tree-act
+#: model:ir.ui.menu,name:crm.menu_crm_segmentation-act
+msgid "Contacts Segmentation"
+msgstr ""
+
+#. module: crm
+#: model:process.node,note:crm.process_node_meeting0
+msgid "Schedule a normal or phone meeting"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.resource.type,name:crm.type_lead1
+msgid "Telesales"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Leads Assigned to Me or My Team(s)"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_crm_segmentation_line
+msgid "Segmentation line"
+msgstr ""
+
+#. module: crm
+#: view:crm.opportunity2phonecall:0
+#: view:crm.phonecall2phonecall:0
+msgid "Planned Date"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Expected Revenues"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Referrer"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,type:0
+#: help:crm.lead.report,type:0
+msgid "Type is used to separate Leads and Opportunities"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead.report,creation_month:0
+#: selection:crm.lead.report,deadline_month:0
+#: selection:crm.phonecall.report,month:0
+msgid "July"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Lead / Customer"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.section,name:crm.crm_case_section_2
+msgid "Support Department"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "Show only lead"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.crm_case_section_act
+#: model:ir.model,name:crm.model_crm_case_section
+#: model:ir.ui.menu,name:crm.menu_crm_case_section_act
+msgid "Sales Teams"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.stage,case_default:0
+msgid "Default to New Sales Team"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Team"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+msgid "Leads/Opportunities which are in New state"
+msgstr ""
+
+#. module: crm
+#: selection:crm.phonecall,state:0
+#: view:crm.phonecall.report:0
+msgid "Not Held"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,probability:0
+msgid "Probability"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+#: view:crm.phonecall.report:0
+#: field:crm.phonecall.report,month:0
+msgid "Month"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: model:ir.actions.act_window,name:crm.crm_case_category_act_leads_all
+#: model:ir.ui.menu,name:crm.menu_crm_leads
+#: model:process.node,name:crm.process_node_leads0
+msgid "Leads"
+msgstr ""
+
+#. module: crm
+#: code:addons/crm/crm_lead.py:579
+#, python-format
+msgid "Merged leads"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor5
+msgid "Design"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead2opportunity.partner,name:0
+#: selection:crm.lead2opportunity.partner.mass,name:0
+msgid "Merge with existing opportunities"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall.report:0
+#: selection:crm.phonecall.report,state:0
+msgid "Todo"
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,name:crm.mt_lead_convert_to_opportunity
+#: model:mail.message.subtype,name:crm.mt_salesteam_lead_opportunity
+msgid "Lead to Opportunity"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,user_email:0
+msgid "User Email"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,partner_name:0
+msgid ""
+"The name of the future partner company that will be created while converting "
+"the lead into opportunity"
+msgstr ""
+
+#. module: crm
+#: field:crm.opportunity2phonecall,note:0
+#: field:crm.phonecall2phonecall,note:0
+msgid "Note"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead,priority:0
+#: selection:crm.lead.report,priority:0
+#: selection:crm.phonecall,priority:0
+#: selection:crm.phonecall.report,priority:0
+msgid "Low"
+msgstr ""
+
+#. module: crm
+#: selection:crm.case.stage,state:0
+#: field:crm.lead,date_closed:0
+#: selection:crm.lead,state:0
+#: view:crm.lead.report:0
+#: selection:crm.lead.report,state:0
+#: field:crm.phonecall,date_closed:0
+msgid "Closed"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Open Opportunities"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.resource.type,name:crm.type_lead2
+msgid "Email Campaign - Services"
+msgstr ""
+
+#. module: crm
+#: selection:crm.case.stage,state:0
+#: selection:crm.lead,state:0
+#: view:crm.lead.report:0
+#: selection:crm.lead.report,state:0
+#: selection:crm.phonecall.report,state:0
+msgid "Pending"
+msgstr ""
+
+#. module: crm
+#: model:process.transition,name:crm.process_transition_leadopportunity0
+msgid "Prospect Opportunity"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,email_cc:0
+msgid "Global CC"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+#: model:ir.actions.act_window,name:crm.crm_case_categ_phone0
+#: model:ir.ui.menu,name:crm.menu_crm_case_phone
+#: model:ir.ui.menu,name:crm.menu_crm_config_phonecall
+msgid "Phone Calls"
+msgstr ""
+
+#. module: crm
+#: view:crm.case.stage:0
+msgid "Stage Search"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead.report,delay_open:0
+#: help:crm.phonecall.report,delay_open:0
+msgid "Number of Days to open the case"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,phone:0
+#: field:crm.opportunity2phonecall,phone:0
+#: view:crm.phonecall:0
+#: field:crm.phonecall,partner_phone:0
+#: field:crm.phonecall2phonecall,phone:0
+msgid "Phone"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.channel,active:0
+#: field:crm.case.section,active:0
+#: field:crm.lead,active:0
+#: field:crm.phonecall,active:0
+msgid "Active"
+msgstr ""
+
+#. module: crm
+#: selection:crm.segmentation.line,operator:0
+msgid "Mandatory Expression"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Send Mail"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead2opportunity.partner,action:0
+#: selection:crm.lead2opportunity.partner.mass,action:0
+#: selection:crm.partner.binding,action:0
+msgid "Create a new customer"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,deadline_day:0
+msgid "Exp. Closing Day"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor2
+msgid "Software"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,change_responsible:0
+msgid "Reassign Escalated"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+#: model:ir.actions.act_window,name:crm.action_report_crm_opportunity
+#: model:ir.ui.menu,name:crm.menu_report_crm_opportunities_tree
+msgid "Opportunities Analysis"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Misc"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: view:crm.lead.report:0
+#: selection:crm.lead.report,state:0
+msgid "Open"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: field:crm.lead,city:0
+msgid "City"
+msgstr ""
+
+#. module: crm
+#: selection:crm.case.stage,type:0
+msgid "Both"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+msgid "Call Done"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+#: field:crm.phonecall,user_id:0
+msgid "Responsible"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.section,name:crm.crm_case_section_3
+msgid "Direct Marketing"
+msgstr ""
+
+#. module: crm
+#: model:crm.case.categ,name:crm.categ_oppor1
+msgid "Product"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,creation_year:0
+msgid "Creation Year"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead2opportunity.partner.mass:0
+msgid "Conversion Options"
+msgstr ""
+
+#. module: crm
+#: view:crm.case.section:0
+msgid ""
+"Follow this salesteam to automatically track the events associated to users "
+"of this team."
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Address"
+msgstr ""
+
+#. module: crm
+#: help:crm.case.section,alias_id:0
+msgid ""
+"The email address associated with this team. New emails received will "
+"automatically create new leads assigned to the team."
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Unassigned Opportunities"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Search Leads"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead.report:0
+#: view:crm.phonecall.report:0
+#: field:crm.phonecall.report,delay_open:0
+msgid "Delay to open"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.crm_case_categ_phone_outgoing0
+#: model:ir.ui.menu,name:crm.menu_crm_case_phone_outbound
+msgid "Scheduled Calls"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,id:0
+msgid "ID"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,type_id:0
+msgid ""
+"From which campaign (seminar, marketing campaign, mass mailing, ...) did "
+"this contact come from?"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_calendar_attendee
+msgid "Attendee information"
+msgstr ""
+
+#. module: crm
+#: view:crm.segmentation:0
+msgid "Segmentation Test"
+msgstr ""
+
+#. module: crm
+#: view:crm.segmentation:0
+msgid "Continue Process"
+msgstr ""
+
+#. module: crm
+#: selection:crm.lead2opportunity.partner,name:0
+#: selection:crm.lead2opportunity.partner.mass,name:0
+#: model:ir.actions.act_window,name:crm.action_crm_lead2opportunity_partner
+msgid "Convert to opportunity"
+msgstr ""
+
+#. module: crm
+#: field:crm.opportunity2phonecall,user_id:0
+#: field:crm.phonecall2phonecall,user_id:0
+msgid "Assign To"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,date_action_last:0
+#: field:crm.phonecall,date_action_last:0
+msgid "Last Action"
+msgstr ""
+
+#. module: crm
+#: field:crm.phonecall,duration:0
+#: field:crm.phonecall.report,duration:0
+msgid "Duration"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.crm_case_categ_phone_outgoing0
+msgid ""
+"
\n"
+" Click to schedule a call \n"
+"
\n"
+" OpenERP allows you to easily define all the calls to be done\n"
+" by your sales team and follow up based on their summary.\n"
+"
\n"
+" You can use the import feature to massively import a new list "
+"of\n"
+" prospects to qualify.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: crm
+#: help:crm.case.stage,fold:0
+msgid ""
+"This stage is not visible, for example in status bar or kanban view, when "
+"there are no records in that stage to display."
+msgstr ""
+
+#. module: crm
+#: field:crm.lead.report,nbr:0
+#: field:crm.phonecall.report,nbr:0
+msgid "# of Cases"
+msgstr ""
+
+#. module: crm
+#: help:crm.phonecall,section_id:0
+msgid "Sales team to which Case belongs to."
+msgstr ""
+
+#. module: crm
+#: model:crm.case.resource.type,name:crm.type_lead6
+msgid "Banner Ads"
+msgstr ""
+
+#. module: crm
+#: field:crm.merge.opportunity,opportunity_ids:0
+msgid "Leads/Opportunities"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,fax:0
+msgid "Fax"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,company_id:0
+#: view:crm.lead.report:0
+#: field:crm.lead.report,company_id:0
+#: field:crm.phonecall,company_id:0
+#: view:crm.phonecall.report:0
+#: field:crm.phonecall.report,company_id:0
+msgid "Company"
+msgstr ""
+
+#. module: crm
+#: selection:crm.segmentation,state:0
+msgid "Running"
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,description:crm.mt_lead_convert_to_opportunity
+msgid "Lead converted into an opportunity"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Unassigned Leads"
+msgstr ""
+
+#. module: crm
+#: model:mail.message.subtype,description:crm.mt_lead_won
+msgid "Opportunity won"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.categ,object_id:0
+msgid "Object Name"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+msgid "Phone Calls Assigned to Me or My Team(s)"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Reset"
+msgstr ""
+
+#. module: crm
+#: view:sale.config.settings:0
+msgid "After-Sale Services"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,message_ids:0
+#: field:crm.lead,message_ids:0
+#: field:crm.phonecall,message_ids:0
+msgid "Messages"
+msgstr ""
+
+#. module: crm
+#: help:crm.lead,channel_id:0
+msgid "Communication channel (mail, direct, phone, ...)"
+msgstr ""
+
+#. module: crm
+#: field:crm.opportunity2phonecall,name:0
+#: field:crm.phonecall2phonecall,name:0
+msgid "Call summary"
+msgstr ""
+
+#. module: crm
+#: selection:crm.case.stage,state:0
+#: selection:crm.lead,state:0
+#: selection:crm.lead.report,state:0
+#: selection:crm.phonecall,state:0
+#: selection:crm.phonecall.report,state:0
+msgid "Cancelled"
+msgstr ""
+
+#. module: crm
+#: field:crm.lead,color:0
+msgid "Color Index"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.action_report_crm_phonecall
+msgid ""
+"From this report, you can analyse the performance of your sales team, based "
+"on their phone calls. You can group or filter the information according to "
+"several criteria and drill down the information, by adding more groups in "
+"the report."
+msgstr ""
+
+#. module: crm
+#: field:crm.case.stage,fold:0
+msgid "Fold by Default"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.stage,state:0
+msgid "Related Status"
+msgstr ""
+
+#. module: crm
+#: field:crm.phonecall,name:0
+msgid "Call Summary"
+msgstr ""
+
+#. module: crm
+#: field:crm.segmentation.line,expr_operator:0
+msgid "Operator"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+#: model:ir.actions.act_window,name:crm.opportunity2phonecall_act
+msgid "Schedule/Log Call"
+msgstr ""
+
+#. module: crm
+#: view:crm.merge.opportunity:0
+msgid "Select Leads/Opportunities"
+msgstr ""
+
+#. module: crm
+#: selection:crm.phonecall,state:0
+msgid "Confirmed"
+msgstr ""
+
+#. module: crm
+#: model:ir.model,name:crm.model_crm_partner_binding
+msgid "Handle partner binding or generation in CRM wizards."
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,name:crm.act_oppor_stage_user
+msgid "Planned Revenue By User and Stage"
+msgstr ""
+
+#. module: crm
+#: view:crm.phonecall:0
+msgid "Confirm"
+msgstr ""
+
+#. module: crm
+#: view:crm.lead:0
+msgid "Unread messages"
+msgstr ""
+
+#. module: crm
+#: field:crm.phonecall.report,section_id:0
+msgid "Section"
+msgstr ""
+
+#. module: crm
+#: selection:crm.segmentation.line,operator:0
+msgid "Optional Expression"
+msgstr ""
+
+#. module: crm
+#: field:crm.case.section,message_follower_ids:0
+#: field:crm.lead,message_follower_ids:0
+#: field:crm.phonecall,message_follower_ids:0
+msgid "Followers"
+msgstr ""
+
+#. module: crm
+#: model:ir.actions.act_window,help:crm.crm_case_category_act_leads_all
+msgid ""
+"
\n"
+" Click to create an unqualified lead.\n"
+"
\n"
+" Use leads if you need a qualification step before creating an\n"
+" opportunity or a customer. It can be a business card you "
+"received,\n"
+" a contact form filled in your website, or a file of unqualified\n"
+" prospects you import, etc.\n"
+"
\n"
+" Once qualified, the lead can be converted into a business\n"
+" opportunity and/or a new customer in your address book.\n"
+"
\n"
+" Click to log the summary of a phone call. \n"
+"
\n"
+" OpenERP allows you to log inbound calls on the fly to track the\n"
+" history of the communication with a customer or to inform "
+"another\n"
+" team member.\n"
+"
\n"
+" In order to follow up on the call, you can trigger a request "
+"for\n"
+" another call, a meeting or an opportunity.\n"
+"
\n"
+" Create specific tags that fit your company's activities\n"
+" to better classify and analyse your leads and "
+"opportunities.\n"
+" Such categories could for instance reflect your product\n"
+" structure or the different types of sales you do.\n"
+"
\n"
+" Use channels to track the soure of your leads and "
+"opportunities. Channels\n"
+" are mostly used in reporting to analyse sales performance\n"
+" related to marketing efforts.\n"
+"
\n"
+" Some examples of channels: company website, phone call\n"
+" campaign, reseller, etc.\n"
+"
+ % endif
+ ]]>
+
+
+
diff --git a/addons/gamification/data/cron.xml b/addons/gamification/data/cron.xml
new file mode 100644
index 00000000000..8585771c97d
--- /dev/null
+++ b/addons/gamification/data/cron.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Run Goal Challenge Checker
+ 1
+ days
+ -1
+
+ gamification.challenge
+ _cron_update
+ ()
+
+
+
\ No newline at end of file
diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml
new file mode 100644
index 00000000000..66dd7be7431
--- /dev/null
+++ b/addons/gamification/data/goal_base.xml
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+ Reminder for Goal Update
+
+ Reminder ${object.name}
+
+
+
You have not updated your progress for the goal ${object.definition_id.name} (currently reached at ${object.completeness}%) for at least ${object.remind_update_delay} days. Do not forget to do it.
+
+
If you have not changed your score yet, you can use the button "The current value is up to date" to indicate so.
+
+ % endfor
+% endif
+ ]]>
+
+
+
+
+
+
+
+
+ Set your Timezone
+ Configure your profile and specify your timezone
+ count
+ boolean
+
+ [('id','=',user.id),('partner_id.tz', '!=', False)]
+
+ user.id
+
+
+
+ Set your Company Data
+ Write some information about your company (specify at least a name)
+ count
+ boolean
+
+ [('user_ids', 'in', [user.id]), ('name', '=', 'Your Company')]
+ lower
+
+ user.company_id.id
+
+
+
+ Set your Company Logo
+ count
+ boolean
+
+ [('user_ids', 'in', user.id),('logo', '!=', False)]
+
+ user.company_id.id
+
+
+
+ Create User
+ ir.actions.act_window
+ res.users
+ form
+ current
+
+ {'default_groups_ref': ['base.group_user']}
+ Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.
+
+
+
+ Invite new Users
+ Create at least another user
+ boolean
+ count
+
+ [('id', '!=', user.id)]
+
+
+
+
+ Mail Group Following
+ Follow mail groups to receive news
+ python
+ result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.group'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)
+
+
+
+
+
+
+ Complete your Profile
+ once
+ personal
+ never
+
+ inprogress
+ other
+
+
+
+ Setup your Company
+ once
+ personal
+ never
+
+ inprogress
+ other
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+ 0
+
+
+
+
+ 1
+
+
+
+
+
diff --git a/addons/gamification/html/index.html b/addons/gamification/html/index.html
new file mode 100644
index 00000000000..890efa0d1ec
--- /dev/null
+++ b/addons/gamification/html/index.html
@@ -0,0 +1,86 @@
+
+
+
+
Drive Engagement with Gamification
+
Leverage natural desire for competition
+
+ Reinforce good habits and improve win rates with real-time recognition and rewards inspired by game mechanics. Align teams around clear business objectives with challenges, personal objectives and team leader boards.
+
+
+
Leaderboards
+
+
+
+
+ Promote leaders and competition amongst sales team with performance ratios.
+
+
+
+
Personnal Objectives
+
+
+
+
+ Assign clear goals to users to align them with the company objectives.
+
+
+
+
Visual Information
+
+
+
+
+ See in an glance the progress of each user.
+
+
+
+
+
+
+
+
+
Create custom Challenges
+
+
+Use predefined goals to generate easily your own challenges. Assign it to a team or individual users. Receive feedback as often as needed: daily, weekly... Repeat it automatically to compare progresses through time.
+
+
+
+
+
+
+
+
+
+
+
+
+
Motivate with Badges
+
+
+
+
+
+
+
+Inspire achievement with recognition of coworker's good work by rewarding badges. These can be deserved manually or upon completion of challenges. Add fun to the competition with rare badges.
+
+
+
+
+
+
+
+
Adapt to any module
+
+
+Create goals linked to any module. The evaluation system is very flexible and can be used for many different tasks : sales evaluation, creation of events, project completion or even helping new users to complete their profile.
+
+
+
+
+
+
+
+
+
diff --git a/addons/gamification/models/__init__.py b/addons/gamification/models/__init__.py
new file mode 100644
index 00000000000..812a08b1e19
--- /dev/null
+++ b/addons/gamification/models/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
+import goal
+import challenge
+import res_users
+import badge
diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py
new file mode 100644
index 00000000000..1d879393ed2
--- /dev/null
+++ b/addons/gamification/models/badge.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp import SUPERUSER_ID
+from openerp.osv import fields, osv
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from openerp.tools.translate import _
+
+from datetime import date
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class gamification_badge_user(osv.Model):
+ """User having received a badge"""
+
+ _name = 'gamification.badge.user'
+ _description = 'Gamification user badge'
+ _order = "create_date desc"
+
+ _columns = {
+ 'user_id': fields.many2one('res.users', string="User", required=True),
+ 'sender_id': fields.many2one('res.users', string="Sender", help="The user who has send the badge"),
+ 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True),
+ 'comment': fields.text('Comment'),
+ 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"),
+ 'create_date': fields.datetime('Created', readonly=True),
+ 'create_uid': fields.many2one('res.users', string='Creator', readonly=True),
+ }
+
+
+ def _send_badge(self, cr, uid, ids, context=None):
+ """Send a notification to a user for receiving a badge
+
+ Does not verify constrains on badge granting.
+ The users are added to the owner_ids (create badge_user if needed)
+ The stats counters are incremented
+ :param ids: list(int) of badge users that will receive the badge
+ """
+ res = True
+ temp_obj = self.pool.get('email.template')
+ user_obj = self.pool.get('res.users')
+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context)
+ for badge_user in self.browse(cr, uid, ids, context=context):
+ body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=context)
+ res = user_obj.message_post(cr, uid, badge_user.user_id.id, body=body_html, context=context)
+ return res
+
+ def create(self, cr, uid, vals, context=None):
+ self.pool.get('gamification.badge').check_granting(cr, uid, badge_id=vals.get('badge_id'), context=context)
+ return super(gamification_badge_user, self).create(cr, uid, vals, context=context)
+
+
+class gamification_badge(osv.Model):
+ """Badge object that users can send and receive"""
+
+ CAN_GRANT = 1
+ NOBODY_CAN_GRANT = 2
+ USER_NOT_VIP = 3
+ BADGE_REQUIRED = 4
+ TOO_MANY = 5
+
+ _name = 'gamification.badge'
+ _description = 'Gamification badge'
+ _inherit = ['mail.thread']
+
+ def _get_owners_info(self, cr, uid, ids, name, args, context=None):
+ """Return:
+ the list of unique res.users ids having received this badge
+ the total number of time this badge was granted
+ the total number of users this badge was granted to
+ """
+ result = dict.fromkeys(ids, False)
+ for obj in self.browse(cr, uid, ids, context=context):
+ res = list(set(owner.user_id.id for owner in obj.owner_ids))
+ result[obj.id] = {
+ 'unique_owner_ids': res,
+ 'stat_count': len(obj.owner_ids),
+ 'stat_count_distinct': len(res)
+ }
+ return result
+
+ def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
+ """Return stats related to badge users"""
+ result = dict.fromkeys(ids, False)
+ badge_user_obj = self.pool.get('gamification.badge.user')
+ first_month_day = date.today().replace(day=1).strftime(DF)
+ for bid in ids:
+ result[bid] = {
+ 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid)], context=context, count=True),
+ 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_date', '>=', first_month_day)], context=context, count=True),
+ 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True),
+ 'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True)
+ }
+ return result
+
+ def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
+ """Computes the number of badges remaining the user can send
+
+ 0 if not allowed or no remaining
+ integer if limited sending
+ -1 if infinite (should not be displayed)
+ """
+ result = dict.fromkeys(ids, False)
+ for badge in self.browse(cr, uid, ids, context=context):
+ if self._can_grant_badge(cr, uid, badge.id, context) != 1:
+ # if the user cannot grant this badge at all, result is 0
+ result[badge.id] = 0
+ elif not badge.rule_max:
+ # if there is no limitation, -1 is returned which means 'infinite'
+ result[badge.id] = -1
+ else:
+ result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
+ return result
+
+ _columns = {
+ 'name': fields.char('Badge', required=True, translate=True),
+ 'description': fields.text('Description'),
+ 'image': fields.binary("Image", help="This field holds the image used for the badge, limited to 256x256"),
+ 'rule_auth': fields.selection([
+ ('everyone', 'Everyone'),
+ ('users', 'A selected list of users'),
+ ('having', 'People having some badges'),
+ ('nobody', 'No one, assigned through challenges'),
+ ],
+ string="Allowance to Grant",
+ help="Who can grant this badge",
+ required=True),
+ 'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users',
+ string='Authorized Users',
+ help="Only these people can give this badge"),
+ 'rule_auth_badge_ids': fields.many2many('gamification.badge',
+ 'rel_badge_badge', 'badge1_id', 'badge2_id',
+ string='Required Badges',
+ help="Only the people having these badges can give this badge"),
+
+ 'rule_max': fields.boolean('Monthly Limited Sending',
+ help="Check to set a monthly limit per person of sending this badge"),
+ 'rule_max_number': fields.integer('Limitation Number',
+ help="The maximum number of time this badge can be sent per month per person."),
+ 'stat_my_monthly_sending': fields.function(_get_badge_user_stats,
+ type="integer",
+ string='My Monthly Sending Total',
+ multi='badge_users',
+ help="The number of time the current user has sent this badge this month."),
+ 'remaining_sending': fields.function(_remaining_sending_calc, type='integer',
+ string='Remaining Sending Allowed', help="If a maxium is set"),
+
+ 'challenge_ids': fields.one2many('gamification.challenge', 'reward_id',
+ string="Reward of Challenges"),
+
+ 'goal_definition_ids': fields.many2many('gamification.goal.definition', 'badge_unlocked_definition_rel',
+ string='Rewarded by',
+ help="The users that have succeeded theses goals will receive automatically the badge."),
+
+ 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id',
+ string='Owners', help='The list of instances of this badge granted to users'),
+ 'active': fields.boolean('Active'),
+ 'unique_owner_ids': fields.function(_get_owners_info,
+ string='Unique Owners',
+ help="The list of unique users having received this badge.",
+ multi='unique_users',
+ type="many2many", relation="res.users"),
+
+ 'stat_count': fields.function(_get_owners_info, string='Total',
+ type="integer",
+ multi='unique_users',
+ help="The number of time this badge has been received."),
+ 'stat_count_distinct': fields.function(_get_owners_info,
+ type="integer",
+ string='Number of users',
+ multi='unique_users',
+ help="The number of time this badge has been received by unique users."),
+ 'stat_this_month': fields.function(_get_badge_user_stats,
+ type="integer",
+ string='Monthly total',
+ multi='badge_users',
+ help="The number of time this badge has been received this month."),
+ 'stat_my': fields.function(_get_badge_user_stats, string='My Total',
+ type="integer",
+ multi='badge_users',
+ help="The number of time the current user has received this badge."),
+ 'stat_my_this_month': fields.function(_get_badge_user_stats,
+ type="integer",
+ string='My Monthly Total',
+ multi='badge_users',
+ help="The number of time the current user has received this badge this month."),
+ }
+
+ _defaults = {
+ 'rule_auth': 'everyone',
+ 'active': True,
+ }
+
+ def check_granting(self, cr, uid, badge_id, context=None):
+ """Check the user 'uid' can grant the badge 'badge_id' and raise the appropriate exception
+ if not
+
+ Do not check for SUPERUSER_ID
+ """
+ status_code = self._can_grant_badge(cr, uid, badge_id, context=context)
+ if status_code == self.CAN_GRANT:
+ return True
+ elif status_code == self.NOBODY_CAN_GRANT:
+ raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.'))
+ elif status_code == self.USER_NOT_VIP:
+ raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.'))
+ elif status_code == self.BADGE_REQUIRED:
+ raise osv.except_osv(_('Warning!'), _('You do not have the required badges.'))
+ elif status_code == self.TOO_MANY:
+ raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.'))
+ else:
+ _logger.exception("Unknown badge status code: %d" % int(status_code))
+ return False
+
+ def _can_grant_badge(self, cr, uid, badge_id, context=None):
+ """Check if a user can grant a badge to another user
+
+ :param uid: the id of the res.users trying to send the badge
+ :param badge_id: the granted badge id
+ :return: integer representing the permission.
+ """
+ if uid == SUPERUSER_ID:
+ return self.CAN_GRANT
+
+ badge = self.browse(cr, uid, badge_id, context=context)
+
+ if badge.rule_auth == 'nobody':
+ return self.NOBODY_CAN_GRANT
+
+ elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]:
+ return self.USER_NOT_VIP
+
+ elif badge.rule_auth == 'having':
+ all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context)
+ for required_badge in badge.rule_auth_badge_ids:
+ if required_badge.id not in all_user_badges:
+ return self.BADGE_REQUIRED
+
+ if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number:
+ return self.TOO_MANY
+
+ # badge.rule_auth == 'everyone' -> no check
+ return self.CAN_GRANT
+
+ def check_progress(self, cr, uid, context=None):
+ try:
+ model, res_id = template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'badge_hidden')
+ except ValueError:
+ return True
+ badge_user_obj = self.pool.get('gamification.badge.user')
+ if not badge_user_obj.search(cr, uid, [('user_id', '=', uid), ('badge_id', '=', res_id)], context=context):
+ values = {
+ 'user_id': uid,
+ 'badge_id': res_id,
+ }
+ badge_user_obj.create(cr, SUPERUSER_ID, values, context=context)
+ return True
\ No newline at end of file
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
new file mode 100644
index 00000000000..ae27aa96719
--- /dev/null
+++ b/addons/gamification/models/challenge.py
@@ -0,0 +1,821 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp import SUPERUSER_ID
+from openerp.osv import fields, osv
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from openerp.tools.translate import _
+
+from datetime import date, datetime, timedelta
+import calendar
+import logging
+_logger = logging.getLogger(__name__)
+
+# display top 3 in ranking, could be db variable
+MAX_VISIBILITY_RANKING = 3
+
+def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
+ """Return the start and end date for a goal period based on today
+
+ :return: (start_date, end_date), datetime.date objects, False if the period is
+ not defined or unknown"""
+ today = date.today()
+ if period == 'daily':
+ start_date = today
+ end_date = start_date
+ elif period == 'weekly':
+ delta = timedelta(days=today.weekday())
+ start_date = today - delta
+ end_date = start_date + timedelta(days=7)
+ elif period == 'monthly':
+ month_range = calendar.monthrange(today.year, today.month)
+ start_date = today.replace(day=1)
+ end_date = today.replace(day=month_range[1])
+ elif period == 'yearly':
+ start_date = today.replace(month=1, day=1)
+ end_date = today.replace(month=12, day=31)
+ else: # period == 'once':
+ start_date = default_start_date # for manual goal, start each time
+ end_date = default_end_date
+
+ if start_date and end_date:
+ return (start_date.strftime(DF), end_date.strftime(DF))
+ else:
+ return (start_date, end_date)
+
+
+class gamification_challenge(osv.Model):
+ """Gamification challenge
+
+ Set of predifined objectives assigned to people with rules for recurrence and
+ rewards
+
+ If 'user_ids' is defined and 'period' is different than 'one', the set will
+ be assigned to the users for each period (eg: every 1st of each month if
+ 'monthly' is selected)
+ """
+
+ _name = 'gamification.challenge'
+ _description = 'Gamification challenge'
+ _inherit = 'mail.thread'
+
+ def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the next report date based on the last report date and report
+ period.
+
+ :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
+ res = {}
+ for challenge in self.browse(cr, uid, ids, context):
+ last = datetime.strptime(challenge.last_report_date, DF).date()
+ if challenge.report_message_frequency == 'daily':
+ next = last + timedelta(days=1)
+ res[challenge.id] = next.strftime(DF)
+ elif challenge.report_message_frequency == 'weekly':
+ next = last + timedelta(days=7)
+ res[challenge.id] = next.strftime(DF)
+ elif challenge.report_message_frequency == 'monthly':
+ month_range = calendar.monthrange(last.year, last.month)
+ next = last.replace(day=month_range[1]) + timedelta(days=1)
+ res[challenge.id] = next.strftime(DF)
+ elif challenge.report_message_frequency == 'yearly':
+ res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
+ # frequency == 'once', reported when closed only
+ else:
+ res[challenge.id] = False
+
+ return res
+
+ def _get_categories(self, cr, uid, context=None):
+ return [
+ ('hr', 'Human Ressources / Engagement'),
+ ('other', 'Settings / Gamification Tools'),
+ ]
+
+ def _get_report_template(self, cr, uid, context=None):
+ try:
+ return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
+ except ValueError:
+ return False
+
+ _order = 'end_date, start_date, name, id'
+ _columns = {
+ 'name': fields.char('Challenge Name', required=True, translate=True),
+ 'description': fields.text('Description', translate=True),
+ 'state': fields.selection([
+ ('draft', 'Draft'),
+ ('inprogress', 'In Progress'),
+ ('done', 'Done'),
+ ],
+ string='State', required=True, track_visibility='onchange'),
+ 'manager_id': fields.many2one('res.users',
+ string='Responsible', help="The user responsible for the challenge."),
+
+ 'user_ids': fields.many2many('res.users', 'user_ids',
+ string='Users',
+ help="List of users participating to the challenge"),
+ 'autojoin_group_id': fields.many2one('res.groups',
+ string='Auto-subscription Group',
+ help='Group of users whose members will be automatically added to user_ids once the challenge is started'),
+
+ 'period': fields.selection([
+ ('once', 'Non recurring'),
+ ('daily', 'Daily'),
+ ('weekly', 'Weekly'),
+ ('monthly', 'Monthly'),
+ ('yearly', 'Yearly')
+ ],
+ string='Periodicity',
+ help='Period of automatic goal assigment. If none is selected, should be launched manually.',
+ required=True),
+ 'start_date': fields.date('Start Date',
+ help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
+ 'end_date': fields.date('End Date',
+ help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
+
+ 'invited_user_ids': fields.many2many('res.users', 'invited_user_ids',
+ string="Suggest to users"),
+
+ 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
+ string='Lines',
+ help="List of goals that will be set",
+ required=True),
+
+ 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
+ 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
+ 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
+ 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
+ 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
+
+ 'visibility_mode': fields.selection([
+ ('personal', 'Individual Goals'),
+ ('ranking', 'Leader Board (Group Ranking)'),
+ ],
+ string="Display Mode", required=True),
+
+ 'report_message_frequency': fields.selection([
+ ('never', 'Never'),
+ ('onchange', 'On change'),
+ ('daily', 'Daily'),
+ ('weekly', 'Weekly'),
+ ('monthly', 'Monthly'),
+ ('yearly', 'Yearly')
+ ],
+ string="Report Frequency", required=True),
+ 'report_message_group_id': fields.many2one('mail.group',
+ string='Send a copy to',
+ help='Group that will receive a copy of the report in addition to the user'),
+ 'report_template_id': fields.many2one('email.template', string="Report Template", required=True),
+ 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
+ help="Never reminded if no value or zero is specified."),
+ 'last_report_date': fields.date('Last Report Date'),
+ 'next_report_date': fields.function(_get_next_report_date,
+ type='date', string='Next Report Date', store=True),
+
+ 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
+ string="Appears in", help="Define the visibility of the challenge through menus", required=True),
+ }
+
+ _defaults = {
+ 'period': 'once',
+ 'state': 'draft',
+ 'visibility_mode': 'personal',
+ 'report_message_frequency': 'never',
+ 'last_report_date': fields.date.today,
+ 'start_date': fields.date.today,
+ 'manager_id': lambda s, cr, uid, c: uid,
+ 'category': 'hr',
+ 'reward_failure': False,
+ 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
+ }
+
+
+ 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('autojoin_group_id'):
+ new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
+
+ if not vals.get('user_ids'):
+ vals['user_ids'] = []
+ vals['user_ids'] += [(4, user.id) for user in new_group.users]
+
+ create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context)
+
+ # subscribe new users to the challenge
+ if vals.get('user_ids'):
+ # done with browse after super to be sure catch all after orm process
+ challenge = self.browse(cr, uid, create_res, context=context)
+ self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
+
+ return create_res
+
+ def write(self, cr, uid, ids, vals, context=None):
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+
+ # add users when change the group auto-subscription
+ if vals.get('autojoin_group_id'):
+ new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
+
+ if not vals.get('user_ids'):
+ vals['user_ids'] = []
+ vals['user_ids'] += [(4, user.id) for user in new_group.users]
+
+ if vals.get('state') == 'inprogress':
+ # starting a challenge
+ if not vals.get('autojoin_group_id'):
+ # starting challenge, add users in autojoin group
+ if not vals.get('user_ids'):
+ vals['user_ids'] = []
+ for challenge in self.browse(cr, uid, ids, context=context):
+ if challenge.autojoin_group_id:
+ vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users]
+
+ 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)
+
+ elif vals.get('state') == 'draft':
+ # resetting progress
+ if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], 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)
+
+ # subscribe new users to the challenge
+ if vals.get('user_ids'):
+ # done with browse after super if changes in groups
+ for challenge in self.browse(cr, uid, ids, context=context):
+ self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
+
+ return write_res
+
+
+ ##### Update #####
+
+ def _cron_update(self, cr, uid, context=None, ids=False):
+ """Daily cron check.
+
+ - Start planned challenges (in draft and with start_date = today)
+ - Create the missing goals (eg: modified the challenge to add lines)
+ - Update every running challenge
+ """
+ # start planned challenges
+ planned_challenge_ids = self.search(cr, uid, [
+ ('state', '=', 'draft'),
+ ('start_date', '<=', fields.date.today())])
+ self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
+
+ # close planned challenges
+ planned_challenge_ids = self.search(cr, uid, [
+ ('state', '=', 'inprogress'),
+ ('end_date', '>=', fields.date.today())])
+ self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
+
+ if not ids:
+ ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
+
+ return self._update_all(cr, uid, ids, context=context)
+
+ def _update_all(self, cr, uid, ids, context=None):
+ """Update the challenges and related goals
+
+ :param list(int) ids: the ids of the challenges to update, if False will
+ update only challenges in progress."""
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+
+ goal_obj = self.pool.get('gamification.goal')
+
+ # we use yesterday to update the goals that just ended
+ yesterday = date.today() - timedelta(days=1)
+ goal_ids = goal_obj.search(cr, uid, [
+ ('challenge_id', 'in', ids),
+ '|',
+ ('state', 'in', ('inprogress', 'inprogress_update')),
+ '&',
+ ('state', 'in', ('reached', 'failed')),
+ '|',
+ ('end_date', '>=', yesterday.strftime(DF)),
+ ('end_date', '=', False)
+ ], context=context)
+ # update every running goal already generated linked to selected challenges
+ goal_obj.update(cr, uid, goal_ids, context=context)
+
+ for challenge in self.browse(cr, uid, ids, context=context):
+ if challenge.autojoin_group_id:
+ # check in case of new users in challenge, this happens if manager removed users in challenge manually
+ self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, 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, [
+ ('challenge_id', '=', challenge.id),
+ ('start_date', '>=', challenge.last_report_date),
+ ('end_date', '<=', challenge.last_report_date)
+ ])
+
+ if len(closed_goals_to_report) > 0:
+ # some goals need a final report
+ self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
+
+ if fields.date.today() == challenge.next_report_date:
+ self.report_progress(cr, uid, challenge, context=context)
+
+ self.check_challenge_reward(cr, uid, ids, context=context)
+ return True
+
+ def quick_update(self, cr, uid, challenge_id, context=None):
+ """Update all the goals of a 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 action_check(self, cr, uid, ids, context=None):
+ """Check a challenge
+
+ Create goals that haven't been created yet (eg: if added users)
+ Recompute the current value for each goal related"""
+ return self._update_all(cr, uid, ids=ids, context=context)
+
+ def action_report_progress(self, cr, uid, ids, context=None):
+ """Manual report of a goal, does not influence automatic report frequency"""
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+ for challenge in self.browse(cr, uid, ids, context):
+ self.report_progress(cr, uid, challenge, context=context)
+ return True
+
+
+ ##### Automatic actions #####
+
+ 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"""
+
+ for challenge in self.browse(cr, uid, ids, context):
+ (start_date, end_date) = start_end_date_for_period(challenge.period)
+
+ # if no periodicity, use challenge dates
+ if not start_date and challenge.start_date:
+ start_date = challenge.start_date
+ if not end_date and challenge.end_date:
+ end_date = challenge.end_date
+
+ for line in challenge.line_ids:
+ 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))
+
+ # goal already existing for this line ?
+ if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
+
+ # resume canceled goals
+ domain.append(('state', '=', 'canceled'))
+ canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
+ goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
+ goal_obj.update(cr, uid, canceled_goal_ids, context=context)
+
+ # skip to next user
+ continue
+
+ values = {
+ 'definition_id': line.definition_id.id,
+ 'line_id': line.id,
+ 'user_id': user.id,
+ 'target_goal': line.target_goal,
+ 'state': 'inprogress',
+ }
+
+ 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)
+
+ goal_obj.update(cr, uid, [new_goal_id], context=context)
+
+ return True
+
+ ##### JS utilities #####
+
+ def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
+ """Return a serialised version of the goals information
+
+ :challenge: browse record of challenge to compute
+ :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
+ :restrict_goal_ids: compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge
+ :restrict_top: for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
+ restrict_goal_ids has priority over restrict_top
+
+ format list
+ # if visibility_mode == 'ranking'
+ {
+ 'name': ,
+ 'description': ,
+ 'condition': ,
+ 'computation_mode': ,
+ 'monetary': <{True,False}>,
+ 'suffix': ,
+ 'action': <{True,False}>,
+ 'display_mode': <{progress,boolean}>,
+ 'target': ,
+ 'own_goal_id': ,
+ 'goals': [
+ {
+ 'id': ,
+ 'rank': ,
+ 'user_id': ,
+ 'name': ,
+ 'state': ,
+ 'completeness': ,
+ 'current': ,
+ }
+ ]
+ },
+ # if visibility_mode == 'personal'
+ {
+ 'id': ,
+ 'name': ,
+ 'description': ,
+ 'condition': ,
+ 'computation_mode': ,
+ 'monetary': <{True,False}>,
+ 'suffix': ,
+ 'action': <{True,False}>,
+ 'display_mode': <{progress,boolean}>,
+ 'target': ,
+ 'state': ,
+ 'completeness': ,
+ 'current': ,
+ }
+ """
+ goal_obj = self.pool.get('gamification.goal')
+ (start_date, end_date) = start_end_date_for_period(challenge.period)
+
+ res_lines = []
+ for line in challenge.line_ids:
+ line_data = {
+ 'name': line.definition_id.name,
+ 'description': line.definition_id.description,
+ 'condition': line.definition_id.condition,
+ 'computation_mode': line.definition_id.computation_mode,
+ 'monetary': line.definition_id.monetary,
+ 'suffix': line.definition_id.suffix,
+ 'action': True if line.definition_id.action_id else False,
+ 'display_mode': line.definition_id.display_mode,
+ 'target': line.target_goal,
+ }
+ domain = [
+ ('line_id', '=', line.id),
+ ('state', '!=', 'draft'),
+ ]
+ if restrict_goal_ids:
+ domain.append(('ids', 'in', restrict_goal_ids))
+ else:
+ # if no subset goals, use the dates for restriction
+ if start_date:
+ domain.append(('start_date', '=', start_date))
+ if end_date:
+ domain.append(('end_date', '=', end_date))
+
+ if challenge.visibility_mode == 'personal':
+ if not user_id:
+ raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
+ domain.append(('user_id', '=', user_id))
+ sorting = goal_obj._order
+ limit = 1
+ # initialise in case search returns no results
+ line_data.update({
+ 'id': 0,
+ 'current': 0,
+ 'completeness': 0,
+ 'state': 'draft',
+ })
+ else:
+ line_data.update({
+ 'own_goal_id': False,
+ 'goals': [],
+ })
+ sorting = "completeness desc, current desc"
+ limit = False
+
+ goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
+ ranking = 0
+ for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
+ if challenge.visibility_mode == 'personal':
+ # limit=1 so only one result
+ line_data.update({
+ 'id': goal.id,
+ 'current': goal.current,
+ 'completeness': goal.completeness,
+ 'state': goal.state,
+ })
+ else:
+ ranking += 1
+ if user_id and goal.user_id.id == user_id:
+ line_data['own_goal_id'] = goal.id
+ elif restrict_top and ranking > restrict_top:
+ # not own goal, over top, skipping
+ continue
+
+ line_data['goals'].append({
+ 'id': goal.id,
+ 'user_id': goal.user_id.id,
+ 'name': goal.user_id.name,
+ 'rank': ranking,
+ 'current': goal.current,
+ 'completeness': goal.completeness,
+ 'state': goal.state,
+ })
+ res_lines.append(line_data)
+ return res_lines
+
+ ##### Reporting #####
+
+ def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
+ """Post report about the progress of the goals
+
+ :param challenge: the challenge object that need to be reported
+ :param users: the list(res.users) of users that are concerned by
+ the report. If False, will send the report to every user concerned
+ (goal users and group that receive a copy). Only used for challenge with
+ a visibility mode set to 'personal'.
+ :param goal_ids: the list(int) of goal ids linked to the challenge for
+ the report. If not specified, use the goals for the current challenge
+ period. This parameter can be used to produce report for previous challenge
+ periods.
+ :param subset_goal_ids: a list(int) of goal ids to restrict the report
+ """
+ if context is None:
+ context = {}
+
+ temp_obj = self.pool.get('email.template')
+ ctx = context.copy()
+ if challenge.visibility_mode == 'ranking':
+ lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
+
+ ctx.update({'challenge_lines': lines_boards})
+ body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
+
+ # send to every follower of the challenge
+ self.message_post(cr, uid, challenge.id,
+ body=body_html,
+ context=context,
+ subtype='mail.mt_comment')
+ if challenge.report_message_group_id:
+ self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
+ body=body_html,
+ context=context,
+ subtype='mail.mt_comment')
+
+ else:
+ # generate individual reports
+ for user in users or challenge.user_ids:
+ goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
+ if not goals:
+ continue
+
+ ctx.update({'challenge_lines': goals})
+ body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
+
+ # send message only to users, not on the challenge
+ self.message_post(cr, uid, 0,
+ body=body_html,
+ partner_ids=[(4, user.partner_id.id)],
+ context=context,
+ subtype='mail.mt_comment')
+ if challenge.report_message_group_id:
+ self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
+ body=body_html,
+ context=context,
+ subtype='mail.mt_comment')
+ return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
+
+ ##### Challenges #####
+ def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
+ """The user accept the suggested challenge"""
+ user_id = user_id or uid
+ user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
+ message = "%s has joined the challenge" % user.name
+ self.message_post(cr, uid, 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, uid, challenge_ids, context=context)
+
+ def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
+ """The user discard the suggested challenge"""
+ user_id = user_id or uid
+ user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
+ message = "%s has refused the challenge" % user.name
+ self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
+ return self.write(cr, uid, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
+
+ def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
+ result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
+ id = result and result[1] or False
+ result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
+ result['res_id'] = challenge_id
+ return result
+
+ def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
+ """Actions for the end of a challenge
+
+ If a reward was selected, grant it to the correct users.
+ Rewards granted at:
+ - the end date for a challenge with no periodicity
+ - the end of a period for challenge with periodicity
+ - when a challenge is manually closed
+ (if no end date, a running challenge is never rewarded)
+ """
+ if isinstance(ids, (int,long)):
+ ids = [ids]
+ context = context or {}
+ for challenge in self.browse(cr, uid, ids, context=context):
+ (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
+ yesterday = date.today() - timedelta(days=1)
+ if end_date == yesterday.strftime(DF) or force:
+ # open chatter message
+ message_body = _("The challenge %s is finished." % challenge.name)
+
+ # reward for everybody succeeding
+ rewarded_users = []
+ if challenge.reward_id:
+ for user in challenge.user_ids:
+ reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
+ ('challenge_id', '=', challenge.id),
+ ('user_id', '=', user.id),
+ ('start_date', '=', start_date),
+ ('end_date', '=', end_date),
+ ('state', '=', 'reached')
+ ], context=context)
+ if len(reached_goal_ids) == len(challenge.line_ids):
+ self.reward_user(cr, uid, user.id, challenge.reward_id.id, context)
+ rewarded_users.append(user)
+
+ if rewarded_users:
+ message_body += _(" Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([user.name for user in rewarded_users])))
+ else:
+ message_body += _(" Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
+
+ # reward bests
+ if challenge.reward_first_id:
+ (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context)
+ if first_user:
+ self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context)
+ message_body += _(" Special rewards were sent to the top competing users. The ranking for this challenge is :")
+ message_body += " 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
+ else:
+ message_body += _("Nobody reached the required conditions to receive special badges.")
+
+ if second_user and challenge.reward_second_id:
+ self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context)
+ message_body += " 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
+ if third_user and challenge.reward_third_id:
+ self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context)
+ message_body += " 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
+
+ self.message_post(cr, uid, challenge.id, body=message_body, context=context)
+ return True
+
+ def get_top3_users(self, cr, uid, challenge, context=None):
+ """Get the top 3 users for a defined challenge
+
+ Ranking criterias:
+ 1. succeed every goal of the challenge
+ 2. total completeness of each goal (can be over 100)
+ Top 3 is computed only for users succeeding every goal of the challenge,
+ except if reward_failure is True, in which case every user is
+ considered.
+ :return: ('first', 'second', 'third'), tuple containing the res.users
+ objects of the top 3 users. If no user meets the criterias for a rank,
+ it is set to False. Nobody can receive a rank is noone receives the
+ higher one (eg: if 'second' == False, 'third' will be False)
+ """
+ goal_obj = self.pool.get('gamification.goal')
+ (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
+ challengers = []
+ for user in challenge.user_ids:
+ all_reached = True
+ total_completness = 0
+ # every goal of the user for the running period
+ goal_ids = goal_obj.search(cr, uid, [
+ ('challenge_id', '=', challenge.id),
+ ('user_id', '=', user.id),
+ ('start_date', '=', start_date),
+ ('end_date', '=', end_date)
+ ], context=context)
+ for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
+ if goal.state != 'reached':
+ all_reached = False
+ if goal.definition_condition == 'higher':
+ # can be over 100
+ total_completness += 100.0 * goal.current / goal.target_goal
+ elif goal.state == 'reached':
+ # for lower goals, can not get percentage so 0 or 100
+ total_completness += 100
+
+ challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
+ sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
+
+ if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
+ # nobody succeeded
+ return (False, False, False)
+ if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
+ # only one user succeeded
+ return (sorted_challengers[0]['user'], False, False)
+ if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
+ # only one user succeeded
+ return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
+ return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
+
+ def reward_user(self, cr, uid, user_id, badge_id, context=None):
+ """Create a badge user and send the badge to him
+
+ :param user_id: the user to reward
+ :param badge_id: the concerned badge
+ """
+ badge_user_obj = self.pool.get('gamification.badge.user')
+ user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
+ return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
+
+
+class gamification_challenge_line(osv.Model):
+ """Gamification challenge line
+
+ Predifined goal for 'gamification_challenge'
+ These are generic list of goals with only the target goal defined
+ Should only be created for the gamification_challenge object
+ """
+
+ _name = 'gamification.challenge.line'
+ _description = 'Gamification generic goal for challenge'
+ _order = "sequence, id"
+
+ def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
+ goal_definition = self.pool.get('gamification.goal.definition')
+ if not definition_id:
+ return {'value': {'definition_id': False}}
+ goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
+ ret = {
+ 'value': {
+ 'condition': goal_definition.condition,
+ 'definition_full_suffix': goal_definition.full_suffix
+ }
+ }
+ return ret
+
+ _columns = {
+ 'name': fields.related('definition_id', 'name', string="Name"),
+ 'challenge_id': fields.many2one('gamification.challenge',
+ string='Challenge',
+ required=True,
+ ondelete="cascade"),
+ 'definition_id': fields.many2one('gamification.goal.definition',
+ string='Goal Definition',
+ required=True,
+ ondelete="cascade"),
+ 'target_goal': fields.float('Target Value to Reach',
+ required=True),
+ 'sequence': fields.integer('Sequence',
+ help='Sequence number for ordering'),
+ 'condition': fields.related('definition_id', 'condition', type="selection",
+ readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
+ 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
+ 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
+ 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),
+ }
+
+ _default = {
+ 'sequence': 1,
+ }
diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py
new file mode 100644
index 00000000000..8792e219d71
--- /dev/null
+++ b/addons/gamification/models/goal.py
@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp import SUPERUSER_ID
+from openerp.osv import fields, osv
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
+from openerp.tools.safe_eval import safe_eval
+from openerp.tools.translate import _
+
+import logging
+import time
+from datetime import date, datetime, timedelta
+
+_logger = logging.getLogger(__name__)
+
+
+class gamification_goal_definition(osv.Model):
+ """Goal definition
+
+ A goal definition contains the way to evaluate an objective
+ Each module wanting to be able to set goals to the users needs to create
+ a new gamification_goal_definition
+ """
+ _name = 'gamification.goal.definition'
+ _description = 'Gamification goal definition'
+
+ def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
+ res = dict.fromkeys(ids, '')
+ for goal in self.browse(cr, uid, ids, context=context):
+ if goal.suffix and not goal.monetary:
+ res[goal.id] = goal.suffix
+ elif goal.monetary:
+ # use the current user's company currency
+ user = self.pool.get('res.users').browse(cr, uid, uid, context)
+ if goal.suffix:
+ res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
+ else:
+ res[goal.id] = user.company_id.currency_id.symbol
+ else:
+ res[goal.id] = ""
+ return res
+
+ _columns = {
+ 'name': fields.char('Goal Definition', required=True, translate=True),
+ 'description': fields.text('Goal Description'),
+ 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."),
+ 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True),
+ 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"),
+ 'computation_mode': fields.selection([
+ ('manually', 'Recorded manually'),
+ ('count', 'Automatic: number of records'),
+ ('sum', 'Automatic: sum on a field'),
+ ('python', 'Automatic: execute a specific Python code'),
+ ],
+ string="Computation Mode",
+ help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
+ required=True),
+ 'display_mode': fields.selection([
+ ('progress', 'Progressive (using numerical values)'),
+ ('boolean', 'Exclusive (done or not-done)'),
+ ],
+ string="Displayed as", required=True),
+ 'model_id': fields.many2one('ir.model',
+ string='Model',
+ help='The model object for the field to evaluate'),
+ 'field_id': fields.many2one('ir.model.fields',
+ string='Field to Sum',
+ help='The field containing the value to evaluate'),
+ 'field_date_id': fields.many2one('ir.model.fields',
+ string='Date Field',
+ help='The date to use for the time period evaluated'),
+ 'domain': fields.char("Filter Domain",
+ help="Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].",
+ required=True),
+ 'compute_code': fields.text('Python Code',
+ help="Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id."),
+ 'condition': fields.selection([
+ ('higher', 'The higher the better'),
+ ('lower', 'The lower the better')
+ ],
+ string='Goal Performance',
+ help='A goal is considered as completed when the current value is compared to the value to reach',
+ required=True),
+ 'action_id': fields.many2one('ir.actions.act_window', string="Action",
+ help="The action that will be called to update the goal value."),
+ 'res_id_field': fields.char("ID Field of user",
+ help="The field name on the user profile (res.users) containing the value for res_id for action.")
+ }
+
+ _defaults = {
+ 'condition': 'higher',
+ 'computation_mode': 'manually',
+ 'domain': "[]",
+ 'monetary': False,
+ 'display_mode': 'progress',
+ }
+
+ def number_following(self, cr, uid, model_name="mail.thread", context=None):
+ """Return the number of 'model_name' objects the user is following
+
+ The model specified in 'model_name' must inherit from mail.thread
+ """
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ return self.pool.get('mail.followers').search(cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context)
+
+
+
+class gamification_goal(osv.Model):
+ """Goal instance for a user
+
+ An individual goal for a user on a specified time period"""
+
+ _name = 'gamification.goal'
+ _description = 'Gamification goal instance'
+ _inherit = 'mail.thread'
+
+ def _get_completion(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the percentage of completeness of the goal, between 0 and 100"""
+ res = dict.fromkeys(ids, 0.0)
+ for goal in self.browse(cr, uid, ids, context=context):
+ if goal.definition_condition == 'higher':
+ if goal.current >= goal.target_goal:
+ res[goal.id] = 100.0
+ else:
+ res[goal.id] = round(100.0 * goal.current / goal.target_goal, 2)
+ elif goal.current < goal.target_goal:
+ # a goal 'lower than' has only two values possible: 0 or 100%
+ res[goal.id] = 100.0
+ else:
+ res[goal.id] = 0.0
+ return res
+
+ def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
+ goal_definition = self.pool.get('gamification.goal.definition')
+ if not definition_id:
+ return {'value': {'definition_id': False}}
+ goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
+ return {'value': {'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition}}
+
+ _columns = {
+ 'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"),
+ 'user_id': fields.many2one('res.users', string='User', required=True),
+ 'line_id': fields.many2one('gamification.challenge.line', string='Goal Line', ondelete="cascade"),
+ 'challenge_id': fields.related('line_id', 'challenge_id',
+ string="Challenge",
+ type='many2one',
+ relation='gamification.challenge',
+ store=True),
+ 'start_date': fields.date('Start Date'),
+ 'end_date': fields.date('End Date'), # no start and end = always active
+ 'target_goal': fields.float('To Reach',
+ required=True,
+ track_visibility='always'), # no goal = global index
+ 'current': fields.float('Current Value', required=True, track_visibility='always'),
+ 'completeness': fields.function(_get_completion, type='float', string='Completeness'),
+ 'state': fields.selection([
+ ('draft', 'Draft'),
+ ('inprogress', 'In progress'),
+ ('inprogress_update', 'In progress (to update)'),
+ ('reached', 'Reached'),
+ ('failed', 'Failed'),
+ ('canceled', 'Canceled'),
+ ],
+ string='State',
+ required=True,
+ track_visibility='always'),
+
+ 'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"),
+ 'remind_update_delay': fields.integer('Remind delay',
+ help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
+ 'last_update': fields.date('Last Update',
+ help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge."),
+
+ 'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True),
+ 'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True),
+ 'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True),
+ 'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True),
+ }
+
+ _defaults = {
+ 'current': 0,
+ 'state': 'draft',
+ 'start_date': fields.date.today,
+ }
+ _order = 'create_date desc, end_date desc, definition_id, id'
+
+ def _check_remind_delay(self, cr, uid, goal, context=None):
+ """Verify if a goal has not been updated for some time and send a
+ reminder message of needed.
+
+ :return: data to write on the goal object
+ """
+ if goal.remind_update_delay and goal.last_update:
+ delta_max = timedelta(days=goal.remind_update_delay)
+ last_update = datetime.strptime(goal.last_update, DF).date()
+ if date.today() - last_update > delta_max and goal.state == 'inprogress':
+ # generate a remind report
+ temp_obj = self.pool.get('email.template')
+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
+ body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
+
+ self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
+ return {'state': 'inprogress_update'}
+ return {}
+
+ def update(self, cr, uid, ids, context=None):
+ """Update the goals to recomputes values and change of states
+
+ If a manual goal is not updated for enough time, the user will be
+ reminded to do so (done only once, in 'inprogress' state).
+ If a goal reaches the target value, the status is set to reached
+ If the end date is passed (at least +1 day, time not considered) without
+ the target value being reached, the goal is set as failed."""
+ if context is None:
+ context = {}
+
+ for goal in self.browse(cr, uid, ids, context=context):
+ towrite = {}
+ if goal.state in ('draft', 'canceled'):
+ # skip if goal draft or canceled
+ continue
+
+ if goal.definition_id.computation_mode == 'manually':
+ towrite.update(self._check_remind_delay(cr, uid, goal, context))
+
+ elif goal.definition_id.computation_mode == 'python':
+ # execute the chosen method
+ cxt = {
+ 'self': self.pool.get('gamification.goal'),
+ 'object': goal,
+ 'pool': self.pool,
+ 'cr': cr,
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'uid': uid,
+ 'result': False,
+ 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
+ }
+ code = goal.definition_id.compute_code.strip()
+ safe_eval(code, cxt, mode="exec", nocopy=True)
+ # the result of the evaluated codeis put in the 'result' local variable, propagated to the context
+ result = cxt.get('result', False)
+ if result and type(result) in (float, int, long):
+ if result != goal.current:
+ towrite['current'] = result
+ else:
+ _logger.exception(_('Invalid return content from the evaluation of %s' % code))
+
+ else: # count or sum
+ obj = self.pool.get(goal.definition_id.model_id.model)
+ field_date_name = goal.definition_id.field_date_id.name
+
+ # eval the domain with user replaced by goal user object
+ domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id})
+
+ # add temporal clause(s) to the domain if fields are filled on the goal
+ if goal.start_date and field_date_name:
+ domain.append((field_date_name, '>=', goal.start_date))
+ if goal.end_date and field_date_name:
+ domain.append((field_date_name, '<=', goal.end_date))
+
+ if goal.definition_id.computation_mode == 'sum':
+ field_name = goal.definition_id.field_id.name
+ res = obj.read_group(cr, uid, domain, [field_name], [''], context=context)
+ new_value = res and res[0][field_name] or 0.0
+
+ else: # computation mode = count
+ new_value = obj.search(cr, uid, domain, context=context, count=True)
+
+ # avoid useless write if the new value is the same as the old one
+ if new_value != goal.current:
+ towrite['current'] = new_value
+
+ # check goal target reached
+ if (goal.definition_condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal):
+ towrite['state'] = 'reached'
+
+ # check goal failure
+ elif goal.end_date and fields.date.today() > goal.end_date:
+ towrite['state'] = 'failed'
+ if towrite:
+ self.write(cr, uid, [goal.id], towrite, context=context)
+ return True
+
+ def action_start(self, cr, uid, ids, context=None):
+ """Mark a goal as started.
+
+ This should only be used when creating goals manually (in draft state)"""
+ self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
+ return self.update(cr, uid, ids, context=context)
+
+ def action_reach(self, cr, uid, ids, context=None):
+ """Mark a goal as reached.
+
+ If the target goal condition is not met, the state will be reset to In
+ Progress at the next goal update until the end date."""
+ return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
+
+ def action_fail(self, cr, uid, ids, context=None):
+ """Set the state of the goal to failed.
+
+ A failed goal will be ignored in future checks."""
+ return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
+
+ def action_cancel(self, cr, uid, ids, context=None):
+ """Reset the completion after setting a goal as reached or failed.
+
+ This is only the current state, if the date and/or target criterias
+ match the conditions for a change of state, this will be applied at the
+ next goal update."""
+ return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
+
+ def create(self, cr, uid, vals, context=None):
+ """Overwrite the create method to add a 'no_remind_goal' field to True"""
+ if context is None:
+ context = {}
+ context['no_remind_goal'] = True
+ return super(gamification_goal, self).create(cr, uid, vals, context=context)
+
+ def write(self, cr, uid, ids, vals, context=None):
+ """Overwrite the write method to update the last_update field to today
+
+ If the current value is changed and the report frequency is set to On
+ change, a report is generated
+ """
+ if context is None:
+ context = {}
+ vals['last_update'] = fields.date.today()
+ result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
+ for goal in self.browse(cr, uid, ids, context=context):
+ if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals):
+ # avoid drag&drop in kanban view
+ raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal'))
+
+ if vals.get('current'):
+ if 'no_remind_goal' in context:
+ # new goals should not be reported
+ continue
+
+ if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange':
+ self.pool.get('gamification.challenge').report_progress(cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context)
+ return result
+
+ def get_action(self, cr, uid, goal_id, context=None):
+ """Get the ir.action related to update the goal
+
+ In case of a manual goal, should return a wizard to update the value
+ :return: action description in a dictionnary
+ """
+ goal = self.browse(cr, uid, goal_id, context=context)
+
+ if goal.definition_id.action_id:
+ # open a the action linked to the goal
+ action = goal.definition_id.action_id.read()[0]
+
+ if goal.definition_id.res_id_field:
+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user})
+
+ # if one element to display, should see it in form mode if possible
+ action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views']
+ return action
+
+ if goal.computation_mode == 'manually':
+ # open a wizard window to update the value manually
+ action = {
+ 'name': _("Update %s") % goal.definition_id.name,
+ 'id': goal_id,
+ 'type': 'ir.actions.act_window',
+ 'views': [[False, 'form']],
+ 'target': 'new',
+ 'context': {'default_goal_id': goal_id, 'default_current': goal.current},
+ 'res_model': 'gamification.goal.wizard'
+ }
+ return action
+
+ return False
diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py
new file mode 100644
index 00000000000..408ddddf082
--- /dev/null
+++ b/addons/gamification/models/res_users.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import osv
+from challenge import MAX_VISIBILITY_RANKING
+
+class res_users_gamification_group(osv.Model):
+ """ Update of res.users class
+ - if adding groups to an user, check gamification.challenge linked to
+ this group, and the user. This is done by overriding the write method.
+ """
+ _name = 'res.users'
+ _inherit = ['res.users']
+
+ def write(self, cr, uid, ids, vals, context=None):
+ """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
+ write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context)
+ if vals.get('groups_id'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
+ user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
+
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
+ if challenge_ids:
+ challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context)
+ return write_res
+
+ def create(self, cr, uid, vals, context=None):
+ """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge"""
+ write_res = super(res_users_gamification_group, self).create(cr, uid, vals, context=context)
+ if vals.get('groups_id'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
+ user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
+
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
+ if challenge_ids:
+ challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, write_res)]}, context=context)
+ return write_res
+
+ # def get_goals_todo_info(self, cr, uid, context=None):
+
+ def get_serialised_gamification_summary(self, cr, uid, context=None):
+ return self._serialised_goals_summary(cr, uid, user_id=uid, context=context)
+
+ def _serialised_goals_summary(self, cr, uid, user_id, context=None):
+ """Return a serialised list of goals assigned to the user, grouped by challenge
+
+ [
+ {
+ 'id': ,
+ 'name': ,
+ 'visibility_mode': ,
+ 'currency': ,
+ 'lines': [(see gamification_challenge._get_serialized_challenge_lines() format)]
+ },
+ ]
+ """
+ all_goals_info = []
+ challenge_obj = self.pool.get('gamification.challenge')
+
+ user = self.browse(cr, uid, uid, context=context)
+ challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
+ for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
+ # serialize goals info to be able to use it in javascript
+ all_goals_info.append({
+ 'id': challenge.id,
+ 'name': challenge.name,
+ 'visibility_mode': challenge.visibility_mode,
+ 'currency': user.company_id.currency_id.id,
+ 'lines': challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context),
+ })
+
+ return all_goals_info
+
+ def get_challenge_suggestions(self, cr, uid, context=None):
+ """Return the list of challenges suggested to the user"""
+ challenge_info = []
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('invited_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
+ for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context):
+ values = {
+ 'id': challenge.id,
+ 'name': challenge.name,
+ 'description': challenge.description,
+ }
+ challenge_info.append(values)
+ return challenge_info
+
+
+class res_groups_gamification_group(osv.Model):
+ """ Update of res.groups class
+ - if adding users from a group, check gamification.challenge linked to
+ this group, and the user. This is done by overriding the write method.
+ """
+ _name = 'res.groups'
+ _inherit = 'res.groups'
+
+ # No need to overwrite create as very unlikely to be the value in the autojoin_group_id field
+ def write(self, cr, uid, ids, vals, context=None):
+ """Overwrite to autosubscribe users if add users to a group marked as autojoin, these will be added to the challenge"""
+ write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context)
+ if vals.get('users'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_ids = [command[1] for command in vals['users'] if command[0] == 4]
+ user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
+
+ challenge_obj = self.pool.get('gamification.challenge')
+ challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context)
+ if challenge_ids:
+ challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context)
+ return write_res
diff --git a/addons/gamification/security/gamification_security.xml b/addons/gamification/security/gamification_security.xml
new file mode 100644
index 00000000000..904ba50721e
--- /dev/null
+++ b/addons/gamification/security/gamification_security.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ Gamification
+
+ 17
+
+
+ Manager
+
+
+
+
+
+ User can only see his/her goals or goal from the same challenge in board visibility
+
+
+
+
+
+
+ [
+ '|',
+ ('user_id','=',user.id),
+ '&',
+ ('challenge_id.user_ids','in',user.id),
+ ('challenge_id.visibility_mode','=','ranking')]
+
+
+
+ Gamification Manager can see any goal
+
+
+
+
+
+
+ [(1, '=', 1)]
+
+
+
+
diff --git a/addons/gamification/security/ir.model.access.csv b/addons/gamification/security/ir.model.access.csv
new file mode 100644
index 00000000000..932c5d97c62
--- /dev/null
+++ b/addons/gamification/security/ir.model.access.csv
@@ -0,0 +1,19 @@
+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
+
+goal_employee,"Goal Employee",model_gamification_goal,base.group_user,1,1,0,0
+goal_manager,"Goal Manager",model_gamification_goal,group_goal_manager,1,1,1,1
+
+goal_definition_employee,"Goal Definition Employee",model_gamification_goal_definition,base.group_user,1,0,0,0
+goal_definition_manager,"Goal Definition Manager",model_gamification_goal_definition,group_goal_manager,1,1,1,1
+
+challenge_employee,"Goal Challenge Employee",model_gamification_challenge,base.group_user,1,0,0,0
+challenge_manager,"Goal Challenge Manager",model_gamification_challenge,group_goal_manager,1,1,1,1
+
+challenge_line_employee,"Challenge Line Employee",model_gamification_challenge_line,base.group_user,1,0,0,0
+challenge_line_manager,"Challenge Line Manager",model_gamification_challenge_line,group_goal_manager,1,1,1,1
+
+badge_employee,"Badge Employee",model_gamification_badge,base.group_user,1,0,0,0
+badge_manager,"Badge Manager",model_gamification_badge,group_goal_manager,1,1,1,1
+
+badge_user_employee,"Badge-user Employee",model_gamification_badge_user,base.group_user,1,1,1,0
+badge_user_manager,"Badge-user Manager",model_gamification_badge_user,group_goal_manager,1,1,1,1
diff --git a/addons/gamification/static/description/icon.png b/addons/gamification/static/description/icon.png
new file mode 100644
index 00000000000..52a75f726ba
Binary files /dev/null and b/addons/gamification/static/description/icon.png differ
diff --git a/addons/gamification/static/src/css/Makefile b/addons/gamification/static/src/css/Makefile
new file mode 100644
index 00000000000..3a6a70cf59b
--- /dev/null
+++ b/addons/gamification/static/src/css/Makefile
@@ -0,0 +1,3 @@
+gamification.css: gamification.sass
+ sass --trace -t expanded gamification.sass gamification.css
+
diff --git a/addons/gamification/static/src/css/gamification.css b/addons/gamification/static/src/css/gamification.css
new file mode 100644
index 00000000000..4ac3a0340fa
--- /dev/null
+++ b/addons/gamification/static/src/css/gamification.css
@@ -0,0 +1,190 @@
+@charset "UTF-8";
+.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_goal {
+ width: 230px;
+ min-height: 200px;
+}
+.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge {
+ width: 250px;
+ min-height: 150px;
+}
+.openerp .oe_kanban_badge_avatars {
+ margin-top: 8px;
+}
+.openerp .oe_kanban_badge_avatars img {
+ width: 30px;
+ height: 30px;
+ padding-left: 0;
+ margin-top: 3px;
+ border-radius: 2px;
+ -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+.openerp .oe_kanban_goal .oe_goal_state_block {
+ width: 200px;
+ height: 130px;
+ margin: auto;
+ margin-bottom: -20px;
+}
+.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state {
+ font-size: 2.5em;
+ font-weight: bold;
+ padding-top: 30px;
+}
+.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state.oe_e {
+ font-size: 7em;
+}
+.openerp .oe_kanban_goal .oe_goal_state_block, .openerp .oe_kanban_goal p {
+ text-align: center;
+}
+.openerp .oe_kanban_content .oe_goal_gauge:first-child {
+ margin: auto;
+}
+.openerp .oe_kanban_content .oe_goal_gauge svg {
+ margin-top: -20px;
+}
+.openerp .oe_no_overflow {
+ overflow: hidden;
+}
+.openerp .oe_red {
+ color: red;
+}
+.openerp .oe_green {
+ color: green;
+}
+.openerp .oe_orange {
+ color: orange;
+}
+.openerp .oe_form td .oe_no_padding {
+ margin-left: -6px;
+}
+.openerp .oe_mail_wall .oe_mail_wall_aside {
+ margin-top: 15px;
+ position: relative;
+ display: inline-block;
+ vertical-align: top;
+ width: 280px;
+ border-radius: 2px;
+}
+.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_challenge_list {
+ background-color: #ededf6;
+}
+.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion {
+ background-color: #d3def1;
+}
+.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion ul {
+ padding-left: 15px;
+}
+.openerp .oe_mail_wall .oe_mail_wall_aside h4, .openerp .oe_mail_wall .oe_mail_wall_aside .oe_goals_list .oe_thead {
+ text-align: center;
+ padding-bottom: 15px;
+}
+.openerp .oe_mail_wall .oe_mail_wall_aside > div {
+ border-bottom: solid 5px white;
+ border-radius: 2px;
+}
+.openerp .oe_mail_wall .oe_goal {
+ border-bottom: solid 3px white;
+ padding: 5px 10px;
+}
+.openerp .oe_mail_wall .oe_goal .oe_update_challenge.oe_e, .openerp .oe_mail_wall .oe_goal .oe_goal_action.oe_e {
+ visibility: hidden;
+ font-size: 25px;
+ float: right;
+ position: relative;
+}
+.openerp .oe_mail_wall .oe_goal div:hover > .oe_update_challenge, .openerp .oe_mail_wall .oe_goal div:hover > .oe_goal_action, .openerp .oe_mail_wall .oe_goal th:hover > .oe_goal_action {
+ visibility: visible;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list {
+ padding-left: 0;
+ margin-top: 5px;
+ margin-bottom: 10px;
+ width: 100%;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress div {
+ display: inline-block;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress::before {
+ content: "•";
+ padding-right: 4px;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before {
+ content: "✓";
+ padding-right: 0;
+ margin-left: -2px;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell {
+ text-decoration: line-through;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell.oe_goal_current {
+ font-size: 150%;
+ font-weight: bold;
+ min-width: 50px;
+ padding: 0 5px;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box {
+ display: inline-block;
+ position: relative;
+ z-index: 0;
+ vertical-align: middle;
+ width: 100%;
+ border: solid 1px rgba(0, 0, 0, 0.03);
+ border-radius: 2px;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress {
+ border: none;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress_background {
+ background-color: white;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ z-index: -2;
+ top: 0;
+ left: 0;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress {
+ background-color: #d4e9de;
+ position: absolute;
+ height: 100%;
+ width: 0;
+ z-index: -1;
+ top: 0;
+ left: 0;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_thead {
+ font-weight: normal;
+ padding: 5px;
+ text-align: center;
+}
+.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell {
+ padding: 3px 0;
+}
+.openerp .oe_mail_wall .oe_table.oe_goals_list .col0 {
+ font-size: 200%;
+ font-weight: bold;
+ width: 25px;
+ text-align: center;
+}
+.openerp .oe_mail_wall .oe_table.oe_goals_list .col1 {
+ padding: 0 5px;
+}
+.openerp .oe_mail_wall .oe_table.oe_goals_list .col2 {
+ width: auto;
+}
+.openerp .oe_mail_wall .oe_user_avatar {
+ width: 24px;
+ padding-right: 5px;
+}
+.openerp .oe_mail_wall .oe_mail {
+ display: inline-block;
+}
+.openerp .oe_mail_wall .oe_table {
+ display: table;
+}
+.openerp .oe_mail_wall .oe_row {
+ display: table-row;
+}
+.openerp .oe_mail_wall .oe_cell {
+ display: table-cell;
+ vertical-align: middle;
+}
diff --git a/addons/gamification/static/src/css/gamification.sass b/addons/gamification/static/src/css/gamification.sass
new file mode 100644
index 00000000000..3bb6b570e66
--- /dev/null
+++ b/addons/gamification/static/src/css/gamification.sass
@@ -0,0 +1,181 @@
+@charset "utf-8"
+
+.openerp
+ // Kanban views
+ .oe_kanban_view
+ .oe_kanban_card.oe_kanban_goal
+ width: 230px
+ min-height: 200px
+ .oe_kanban_card.oe_kanban_badge
+ width: 250px
+ min-height: 150px
+ .oe_kanban_badge_avatars
+ margin-top: 8px
+ img
+ width: 30px
+ height: 30px
+ padding-left: 0
+ margin-top: 3px
+ border-radius: 2px
+ -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2)
+
+ .oe_kanban_goal
+ .oe_goal_state_block
+ width: 200px
+ height: 130px
+ margin: auto
+ margin-bottom: -20px
+ .oe_goal_state
+ font-size: 2.5em
+ font-weight: bold
+ padding-top: 30px
+ .oe_goal_state.oe_e
+ font-size: 7em
+ .oe_goal_state_block,p
+ text-align: center
+
+ .oe_kanban_content
+ .oe_goal_gauge:first-child
+ margin: auto /* avoid margin-right: 16px */
+ .oe_goal_gauge
+ svg
+ margin-top: -20px
+ .oe_no_overflow
+ overflow: hidden
+
+
+ .oe_red
+ color: red
+ .oe_green
+ color: green
+ .oe_orange
+ color: orange
+
+
+ // compensate padding from .openerp .oe_form td.oe_form_group_cell + .oe_form_group_cell
+ .oe_form td .oe_no_padding
+ margin-left: -6px
+
+
+ .oe_mail_wall
+ .oe_mail_wall_aside
+ margin-top: 15px
+ position: relative
+ display: inline-block
+ vertical-align: top
+ width: 280px
+ border-radius: 2px
+
+ .oe_gamification_challenge_list
+ background-color: #EDEDF6
+ .oe_gamification_suggestion
+ background-color: rgb(211, 222, 241)
+ ul
+ padding-left: 15px
+
+ h4, .oe_goals_list .oe_thead
+ text-align: center
+ padding-bottom: 15px
+
+ .oe_mail_wall_aside > div
+ border-bottom: solid 5px white
+ border-radius: 2px
+
+ .oe_goal
+ border-bottom: solid 3px white
+ padding: 5px 10px
+ .oe_update_challenge.oe_e, .oe_goal_action.oe_e
+ visibility: hidden
+ font-size: 25px
+ float: right
+ position: relative
+ div:hover > .oe_update_challenge, div:hover > .oe_goal_action, th:hover > .oe_goal_action
+ visibility: visible
+
+ .oe_goals_list
+ padding-left: 0
+ margin-top: 5px
+ margin-bottom: 10px
+ width: 100%
+
+ .oe_no_progress
+ div
+ display: inline-block
+ .oe_no_progress::before
+ content: "•"
+ padding-right: 4px
+ .oe_no_progress.oe_goal_reached::before
+ content: "✓"
+ padding-right: 0
+ margin-left: -2px
+ .oe_no_progress.oe_goal_reached
+ .oe_cell
+ text-decoration: line-through
+
+ .oe_cell.oe_goal_current
+ font-size: 150%
+ font-weight: bold
+ min-width: 50px
+ padding: 0 5px
+
+ .oe_goal_outer_box
+ display: inline-block
+ position: relative
+ z-index: 0
+ vertical-align: middle
+ width: 100%
+ border: solid 1px rgba(0,0,0,0.03)
+ border-radius: 2px
+ .oe_goal_outer_box.oe_no_progress
+ border: none
+
+ .oe_goal_progress_background
+ background-color: white
+ position: absolute
+ height: 100%
+ width: 100%
+ z-index: -2
+ top: 0
+ left: 0
+
+ .oe_goal_progress
+ background-color: rgb(212, 233, 222)
+ position: absolute
+ height: 100%
+ width: 0
+ z-index: -1
+ top: 0
+ left: 0
+
+ .oe_thead
+ font-weight: normal
+ padding: 5px
+ text-align: center
+ .oe_cell
+ padding: 3px 0
+
+ .oe_table.oe_goals_list
+ .col0
+ font-size: 200%
+ font-weight: bold
+ width: 25px
+ text-align: center
+ .col1
+ padding: 0 5px
+ .col2
+ width: auto
+
+ .oe_user_avatar
+ width: 24px
+ padding-right: 5px
+
+ .oe_mail
+ display: inline-block
+
+ .oe_table
+ display: table
+ .oe_row
+ display: table-row
+ .oe_cell
+ display: table-cell
+ vertical-align: middle
diff --git a/addons/gamification/static/src/js/gamification.js b/addons/gamification/static/src/js/gamification.js
new file mode 100644
index 00000000000..2a8db5f369d
--- /dev/null
+++ b/addons/gamification/static/src/js/gamification.js
@@ -0,0 +1,148 @@
+openerp.gamification = function(instance) {
+ var QWeb = instance.web.qweb;
+
+ instance.gamification.Sidebar = instance.web.Widget.extend({
+ template: 'gamification.UserWallSidebar',
+ init: function (parent, action) {
+ var self = this;
+ this._super(parent, action);
+ this.deferred = $.Deferred();
+ this.goals_info = {};
+ this.challenge_suggestions = {};
+ $(document).off('keydown.klistener');
+ },
+ events: {
+ // update a challenge and related goals
+ 'click a.oe_update_challenge': function(event) {
+ var self = this;
+ var challenge_id = parseInt(event.currentTarget.id, 10);
+ var goals_updated = new instance.web.Model('gamification.challenge').call('quick_update', [challenge_id]);
+ $.when(goals_updated).done(function() {
+ self.get_goal_todo_info();
+ });
+ },
+ // action to modify a goal
+ 'click a.oe_goal_action': function(event) {
+ var self = this;
+ var goal_id = parseInt(event.currentTarget.id, 10);
+ var goal_action = new instance.web.Model('gamification.goal').call('get_action', [goal_id]).then(function(res) {
+ goal_action['action'] = res;
+ });
+ $.when(goal_action).done(function() {
+ var action = self.do_action(goal_action.action);
+ $.when(action).done(function () {
+ new instance.web.Model('gamification.goal').call('update', [[goal_id]]).then(function(res) {
+ self.get_goal_todo_info();
+ });
+ });
+ });
+ },
+ // get more info about a challenge request
+ 'click a.oe_challenge_reply': function(event) {
+ var self = this;
+ var challenge_id = parseInt(event.currentTarget.id, 10);
+ var challenge_action = new instance.web.Model('gamification.challenge').call('reply_challenge_wizard', [challenge_id]).then(function(res) {
+ challenge_action['action'] = res;
+ });
+ $.when(challenge_action).done(function() {
+ self.do_action(challenge_action.action).done(function () {
+ self.get_goal_todo_info();
+ });
+ });
+ },
+ 'click .oe_goal h4': function(event) {
+ var self = this;
+ this.kkeys = [];
+ $(document).on('keydown.klistener', function(event) {
+ if ("37,38,39,40,65,66".indexOf(event.keyCode) < 0) {
+ $(document).off('keydown.klistener');
+ } else {
+ self.kkeys.push(event.keyCode);
+ if (self.kkeys.toString().indexOf("38,38,40,40,37,39,37,39,66,65") >= 0) {
+ new instance.web.Model('gamification.badge').call('check_progress', []);
+ $(document).off('keydown.klistener');
+ }
+ }
+ });
+ }
+ },
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ self.get_goal_todo_info();
+ self.get_challenge_suggestions();
+ },
+ get_goal_todo_info: function() {
+ var self = this;
+ var challenges = new instance.web.Model('res.users').call('get_serialised_gamification_summary', []).then(function(result) {
+ if (result.length === 0) {
+ self.$el.find(".oe_gamification_challenge_list").hide();
+ } else {
+ self.$el.find(".oe_gamification_challenge_list").empty();
+ _.each(result, function(item){
+ var $item = $(QWeb.render("gamification.ChallengeSummary", {challenge: item}));
+ self.render_money_fields($item);
+ self.render_user_avatars($item);
+ self.$el.find('.oe_gamification_challenge_list').append($item);
+ });
+ }
+ });
+ },
+ get_challenge_suggestions: function() {
+ var self = this;
+ var challenge_suggestions = new instance.web.Model('res.users').call('get_challenge_suggestions', []).then(function(result) {
+ if (result.length === 0) {
+ self.$el.find(".oe_gamification_suggestion").hide();
+ } else {
+ var $item = $(QWeb.render("gamification.ChallengeSuggestion", {challenges: result}));
+ self.$el.find('.oe_gamification_suggestion').append($item);
+ }
+ });
+ },
+ render_money_fields: function(item) {
+ var self = this;
+ self.dfm = new instance.web.form.DefaultFieldManager(self);
+ // Generate a FieldMonetary for each .oe_goal_field_monetary
+ item.find(".oe_goal_field_monetary").each(function() {
+ var currency_id = parseInt( $(this).attr('data-id'), 10);
+ money_field = new instance.web.form.FieldMonetary(self.dfm, {
+ attrs: {
+ modifiers: '{"readonly": true}'
+ }
+ });
+ money_field.set('currency', currency_id);
+ money_field.get_currency_info();
+ money_field.set('value', parseInt($(this).text(), 10));
+ money_field.replace($(this));
+ });
+ },
+ render_user_avatars: function(item) {
+ var self = this;
+ item.find(".oe_user_avatar").each(function() {
+ var user_id = parseInt( $(this).attr('data-id'), 10);
+ var url = instance.session.url('/web/binary/image', {model: 'res.users', field: 'image_small', id: user_id});
+ $(this).attr("src", url);
+ });
+ }
+ });
+
+ instance.mail.Widget.include({
+ start: function() {
+ this._super();
+ var sidebar = new instance.gamification.Sidebar(this);
+ sidebar.appendTo($('.oe_mail_wall_aside'));
+ },
+ });
+
+ instance.web_kanban.KanbanRecord.include({
+ // open related goals when clicking on challenge kanban view
+ on_card_clicked: function() {
+ if (this.view.dataset.model === 'gamification.challenge') {
+ this.$('.oe_kanban_project_list a').first().click();
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ });
+
+};
diff --git a/addons/gamification/static/src/xml/gamification.xml b/addons/gamification/static/src/xml/gamification.xml
new file mode 100644
index 00000000000..6316ca886f0
--- /dev/null
+++ b/addons/gamification/static/src/xml/gamification.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
diff --git a/addons/gamification/tests/__init__.py b/addons/gamification/tests/__init__.py
new file mode 100644
index 00000000000..9cafd242b28
--- /dev/null
+++ b/addons/gamification/tests/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2013 OpenERP S.A.
+#
+# 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 .
+#
+##############################################################################
+
+from . import test_challenge
+
+checks = [
+ test_challenge,
+]
diff --git a/addons/gamification/tests/test_challenge.py b/addons/gamification/tests/test_challenge.py
new file mode 100644
index 00000000000..6b93da727ac
--- /dev/null
+++ b/addons/gamification/tests/test_challenge.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2013 OpenERP S.A.
+#
+# 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 .
+#
+##############################################################################
+
+from openerp.tests import common
+
+
+class test_challenge(common.TransactionCase):
+
+ def setUp(self):
+ super(test_challenge, self).setUp()
+ cr, uid = self.cr, self.uid
+ self.data_obj = self.registry('ir.model.data')
+ self.user_obj = self.registry('res.users')
+
+ self.challenge_obj = self.registry('gamification.challenge')
+ self.line_obj = self.registry('gamification.challenge.line')
+ self.goal_obj = self.registry('gamification.goal')
+ self.badge_obj = self.registry('gamification.badge')
+ self.badge_user_obj = self.registry('gamification.badge.user')
+
+ self.demo_user_id = self.data_obj.get_object_reference(cr, uid, 'base', 'user_demo')[1]
+ self.group_user_id = self.data_obj.get_object_reference(cr, uid, 'base', 'group_user')[1]
+ self.challenge_base_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'challenge_base_discover')[1]
+ self.definition_timezone_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'definition_base_timezone')[1]
+ self.badge_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'badge_good_job')[1]
+
+ def test_00_join_challenge(self):
+ cr, uid, context = self.cr, self.uid, {}
+
+ user_ids = self.user_obj.search(cr, uid, [('groups_id', '=', self.group_user_id)])
+ challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
+
+ self.assertGreaterEqual(len(challenge.user_ids), len(user_ids), "Not enough users in base challenge")
+
+ self.user_obj.create(cr, uid, {
+ 'name': 'R2D2',
+ 'login': 'r2d2@openerp.com',
+ 'email': 'r2d2@openerp.com',
+ 'groups_id': [(6, 0, [self.group_user_id])]
+ }, {'no_reset_password': True})
+
+ challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
+ self.assertGreaterEqual(len(challenge.user_ids), len(user_ids)+1, "These are not droids you are looking for")
+
+ def test_10_reach_challenge(self):
+ cr, uid, context = self.cr, self.uid, {}
+
+ self.challenge_obj.write(cr, uid, [self.challenge_base_id], {'state': 'inprogress'}, context=context)
+ challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
+ challenge_user_ids = [user.id for user in challenge.user_ids]
+
+ self.assertEqual(challenge.state, 'inprogress', "Challenge failed the change of state")
+
+ line_ids = self.line_obj.search(cr, uid, [('challenge_id', '=', self.challenge_base_id)], context=context)
+ goal_ids = self.goal_obj.search(cr, uid, [('challenge_id', '=', self.challenge_base_id), ('state', '!=', 'draft')], context=context)
+ self.assertEqual(len(goal_ids), len(line_ids)*len(challenge_user_ids), "Incorrect number of goals generated, should be 1 goal per user, per challenge line")
+
+ # demo user will set a timezone
+ self.user_obj.write(cr, uid, self.demo_user_id, {'tz': "Europe/Brussels"}, context=context)
+ goal_ids = self.goal_obj.search(cr, uid, [('user_id', '=', self.demo_user_id), ('definition_id', '=', self.definition_timezone_id)], context=context)
+
+ self.goal_obj.update(cr, uid, goal_ids, context=context)
+ reached_goal_ids = self.goal_obj.search(cr, uid, [('id', 'in', goal_ids), ('state', '=', 'reached')], context=context)
+ self.assertEqual(set(goal_ids), set(reached_goal_ids), "Not every goal was reached after changing timezone")
+
+ # reward for two firsts as admin may have timezone
+ self.challenge_obj.write(cr, uid, self.challenge_base_id, {'reward_first_id': self.badge_id, 'reward_second_id': self.badge_id}, context=context)
+ self.challenge_obj.write(cr, uid, self.challenge_base_id, {'state': 'done'}, context=context)
+
+ badge_ids = self.badge_user_obj.search(cr, uid, [('badge_id', '=', self.badge_id), ('user_id', '=', self.demo_user_id)])
+ self.assertGreater(len(badge_ids), 0, "Demo user has not received the badge")
\ No newline at end of file
diff --git a/addons/gamification/views/badge.xml b/addons/gamification/views/badge.xml
new file mode 100644
index 00000000000..0b37e62051d
--- /dev/null
+++ b/addons/gamification/views/badge.xml
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+ Badges
+ gamification.badge
+ kanban,tree,form
+
+
+ Click to create a badge.
+
+
+ A badge is a symbolic token granted to a user as a sign of reward.
+ It can be deserved automatically when some conditions are met or manually by users.
+ Some badges are harder than others to get with specific conditions.
+
+ Assign a list of goals to chosen users to evaluate them.
+ The challenge can use a period (weekly, monthly...) for automatic creation of goals.
+ The goals are created for the specified users or member of the group.
+
+ A goal definition is a technical model of goal defining a condition to reach.
+ The dates, values to reach or users are defined in goal instance.
+
+
+
+
+
+ Goal Definitions List
+ gamification.goal.definition
+
+
+
+
+
+
+
+
+
+
+ Goal Definitions Form
+ gamification.goal.definition
+
+
+
+
+
+
+ Goal Definition Search
+ gamification.goal.definition
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/gamification/wizard/__init__.py b/addons/gamification/wizard/__init__.py
new file mode 100644
index 00000000000..638fbef4373
--- /dev/null
+++ b/addons/gamification/wizard/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
+import update_goal
+import grant_badge
diff --git a/addons/gamification/wizard/grant_badge.py b/addons/gamification/wizard/grant_badge.py
new file mode 100644
index 00000000000..44f739fff4e
--- /dev/null
+++ b/addons/gamification/wizard/grant_badge.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
+
+
+class grant_badge_wizard(osv.TransientModel):
+ """ Wizard allowing to grant a badge to a user"""
+
+ _name = 'gamification.badge.user.wizard'
+ _columns = {
+ 'user_id': fields.many2one("res.users", string='User', required=True),
+ 'badge_id': fields.many2one("gamification.badge", string='Badge', required=True),
+ 'comment': fields.text('Comment'),
+ }
+
+ def action_grant_badge(self, cr, uid, ids, context=None):
+ """Wizard action for sending a badge to a chosen user"""
+
+ badge_obj = self.pool.get('gamification.badge')
+ badge_user_obj = self.pool.get('gamification.badge.user')
+
+ for wiz in self.browse(cr, uid, ids, context=context):
+ if uid == wiz.user_id.id:
+ raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself'))
+
+ #create the badge
+ values = {
+ 'user_id': wiz.user_id.id,
+ 'sender_id': uid,
+ 'badge_id': wiz.badge_id.id,
+ 'comment': wiz.comment,
+ }
+ badge_user = badge_user_obj.create(cr, uid, values, context=context)
+ result = badge_obj._send_badge(cr, uid, badge_user, context=context)
+
+ return result
diff --git a/addons/gamification/wizard/grant_badge.xml b/addons/gamification/wizard/grant_badge.xml
new file mode 100644
index 00000000000..e03dbac4096
--- /dev/null
+++ b/addons/gamification/wizard/grant_badge.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+ Grant Badge User Form
+ gamification.badge.user.wizard
+
+
+
+
+
+
+
+
+
diff --git a/addons/gamification/wizard/update_goal.py b/addons/gamification/wizard/update_goal.py
new file mode 100644
index 00000000000..cbda3fefbce
--- /dev/null
+++ b/addons/gamification/wizard/update_goal.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+
+class goal_manual_wizard(osv.TransientModel):
+ """Wizard to update a manual goal"""
+ _name = 'gamification.goal.wizard'
+ _columns = {
+ 'goal_id': fields.many2one("gamification.goal", string='Goal', required=True),
+ 'current': fields.float('Current'),
+ }
+
+ def action_update_current(self, cr, uid, ids, context=None):
+ """Wizard action for updating the current value"""
+
+ goal_obj = self.pool.get('gamification.goal')
+
+ for wiz in self.browse(cr, uid, ids, context=context):
+ towrite = {
+ 'current': wiz.current,
+ 'goal_id': wiz.goal_id.id,
+ }
+ goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context)
+ goal_obj.update(cr, uid, [wiz.goal_id.id], context=context)
+ return {}
diff --git a/addons/gamification/wizard/update_goal.xml b/addons/gamification/wizard/update_goal.xml
new file mode 100644
index 00000000000..793efab90ff
--- /dev/null
+++ b/addons/gamification/wizard/update_goal.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Update the current value of the Goal
+ gamification.goal.wizard
+
+
+
+
+
+
+
diff --git a/addons/gamification_sale_crm/__init__.py b/addons/gamification_sale_crm/__init__.py
new file mode 100644
index 00000000000..e14c014a1ef
--- /dev/null
+++ b/addons/gamification_sale_crm/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
diff --git a/addons/gamification_sale_crm/__openerp__.py b/addons/gamification_sale_crm/__openerp__.py
new file mode 100644
index 00000000000..b2ec9a10428
--- /dev/null
+++ b/addons/gamification_sale_crm/__openerp__.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+{
+ 'name': 'CRM Gamification',
+ 'version': '1.0',
+ 'author': 'OpenERP SA',
+ 'category': 'hidden',
+ 'depends': ['gamification','sale_crm'],
+ 'description': """Example of goal definitions and challenges that can be used related to the usage of the CRM Sale module.""",
+
+ 'data': ['sale_crm_goals.xml'],
+ 'demo': ['sale_crm_goals_demo.xml'],
+ 'auto_install': True,
+}
diff --git a/addons/gamification_sale_crm/sale_crm_goals.xml b/addons/gamification_sale_crm/sale_crm_goals.xml
new file mode 100644
index 00000000000..b48cc8a3117
--- /dev/null
+++ b/addons/gamification_sale_crm/sale_crm_goals.xml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+ Total Invoiced
+
+ sum
+ True
+
+
+
+ [('state','!=','cancel'),('user_id','=',user.id),('type','=','out_invoice')]
+
+
+
+ New Leads
+ Based on the creation date
+ count
+ leads
+
+
+
+ [('user_id','=',user.id), '|', ('type', '=', 'lead'), ('type', '=', 'opportunity')]
+
+
+
+ Time to Qualify a Lead
+ The average number of days to open the case (lower than)
+ sum
+ lower
+ days
+
+
+
+ [('user_id','=',user.id),('type', '=', 'lead')]
+
+
+
+ Days to Close a Deal
+ The average number of days to close the case (lower than)
+ sum
+ lower
+ days
+
+
+
+ [('user_id','=',user.id)]
+
+
+
+
+ Logged Calls
+ Log a certain number of calls to reach this goal
+ count
+ calls
+
+
+ [('user_id','=',user.id),('state','=','done')]
+
+
+
+ New Opportunities
+ Based on the opening date
+ count
+ opportunities
+
+
+ [('user_id','=',user.id),('type','=','opportunity')]
+
+
+
+ New Sales Orders
+ Based on the creation date
+ count
+ orders
+
+
+ [('user_id','=',user.id),('state','not in',('draft', 'sent', 'cancel'))]
+
+
+
+ Paid Sales Orders
+ Based on the invoice date
+ count
+ orders
+
+
+ [('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')]
+
+
+ Total Paid Sales Orders
+ Based on the invoice date
+ count
+ True
+
+
+
+ [('state','=','paid'),('user_id','=',user.id),('type','=','out_invoice')]
+
+
+
+
+ Customer Refunds
+ Refund the least customers (lower than)
+ count
+ lower
+ invoices
+
+
+ [('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')]
+
+
+ Total Customer Refunds
+ The total refunded value is a negative value. Validated when higher (min refunded).
+ sum
+ higher
+ True
+
+
+
+ [('state','!=','cancel'),('user_id','=',user.id),('type','=','out_refund')]
+
+
+
+
+
+
+ Monthly Sales Targets
+ monthly
+ ranking
+
+ weekly
+
+
+
+ Lead Acquisition
+ monthly
+ ranking
+
+ weekly
+
+
+
+
+
+ 20000
+
+
+
+
+
+
+ 7
+
+ 1
+
+
+
+ 15
+
+ 2
+
+
+
+ 5
+
+ 3
+
+
+
+
diff --git a/addons/gamification_sale_crm/sale_crm_goals_demo.xml b/addons/gamification_sale_crm/sale_crm_goals_demo.xml
new file mode 100644
index 00000000000..fd69c36162f
--- /dev/null
+++ b/addons/gamification_sale_crm/sale_crm_goals_demo.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ inprogress
+
+
+
+
+
+
+
+
+
+ 2000
+ inprogress
+
+
+
+
diff --git a/addons/hr/i18n/zh_TW.po b/addons/hr/i18n/zh_TW.po
index e31a3c399fd..60dcb30a743 100644
--- a/addons/hr/i18n/zh_TW.po
+++ b/addons/hr/i18n/zh_TW.po
@@ -7,24 +7,24 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.4\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
-"PO-Revision-Date: 2012-05-10 17:48+0000\n"
-"Last-Translator: Fabien (Open ERP) \n"
+"PO-Revision-Date: 2013-12-27 05:02+0000\n"
+"Last-Translator: Andy Cheng \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2013-09-12 05:56+0000\n"
-"X-Generator: Launchpad (build 16761)\n"
+"X-Launchpad-Export-Date: 2013-12-27 05:22+0000\n"
+"X-Generator: Launchpad (build 16877)\n"
#. module: hr
#: model:process.node,name:hr.process_node_openerpuser0
msgid "Openerp user"
-msgstr ""
+msgstr "Openerp 使用者"
#. module: hr
#: field:hr.config.settings,module_hr_timesheet_sheet:0
msgid "Allow timesheets validation by managers"
-msgstr ""
+msgstr "允許主管批准工時表"
#. module: hr
#: field:hr.job,requirements:0
@@ -62,7 +62,7 @@ msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Time Tracking"
-msgstr ""
+msgstr "追蹤時間"
#. module: hr
#: view:hr.employee:0
@@ -73,17 +73,17 @@ msgstr "分組根據..."
#. module: hr
#: model:ir.actions.act_window,name:hr.view_department_form_installer
msgid "Create Your Departments"
-msgstr ""
+msgstr "建立您的部門"
#. module: hr
#: help:hr.job,no_of_employee:0
msgid "Number of employees currently occupying this job position."
-msgstr ""
+msgstr "從事此職位的現有員工數量。"
#. module: hr
#: field:hr.config.settings,module_hr_evaluation:0
msgid "Organize employees periodic evaluation"
-msgstr ""
+msgstr "安排員工定期考評"
#. module: hr
#: view:hr.department:0
@@ -98,19 +98,19 @@ msgstr "部門"
#. module: hr
#: field:hr.employee,work_email:0
msgid "Work Email"
-msgstr ""
+msgstr "工作電子郵件"
#. module: hr
#: help:hr.employee,image:0
msgid ""
"This field holds the image used as photo for the employee, limited to "
"1024x1024px."
-msgstr ""
+msgstr "此欄位存放圖片作為員工照片,限制大小為 1024*1024 像素。"
#. module: hr
#: help:hr.config.settings,module_hr_holidays:0
msgid "This installs the module hr_holidays."
-msgstr ""
+msgstr "此功能將安裝 hr_holidays 模組。"
#. module: hr
#: view:hr.job:0
@@ -125,7 +125,7 @@ msgstr "正招聘"
#. module: hr
#: field:hr.job,message_unread:0
msgid "Unread Messages"
-msgstr ""
+msgstr "未讀訊息"
#. module: hr
#: field:hr.department,company_id:0
@@ -143,7 +143,7 @@ msgstr ""
#. module: hr
#: field:res.users,employee_ids:0
msgid "Related employees"
-msgstr ""
+msgstr "相關員工"
#. module: hr
#: constraint:hr.employee.category:0
@@ -153,28 +153,28 @@ msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_recruitment:0
msgid "This installs the module hr_recruitment."
-msgstr ""
+msgstr "此功能將安裝 hr_recruitment 模組。"
#. module: hr
#: view:hr.employee:0
msgid "Birth"
-msgstr ""
+msgstr "生日"
#. module: hr
#: model:ir.actions.act_window,name:hr.open_view_categ_form
#: model:ir.ui.menu,name:hr.menu_view_employee_category_form
msgid "Employee Tags"
-msgstr ""
+msgstr "員工標籤"
#. module: hr
#: view:hr.job:0
msgid "Launch Recruitement"
-msgstr ""
+msgstr "啟動招募"
#. module: hr
#: model:process.transition,name:hr.process_transition_employeeuser0
msgid "Link a user to an employee"
-msgstr ""
+msgstr "將一個使用者帳號連結到一位員工"
#. module: hr
#: field:hr.department,parent_id:0
@@ -184,7 +184,7 @@ msgstr "上級部門"
#. module: hr
#: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_config
msgid "Leaves"
-msgstr ""
+msgstr "休假"
#. module: hr
#: selection:hr.employee,marital:0
@@ -194,37 +194,37 @@ msgstr "已婚"
#. module: hr
#: field:hr.job,message_ids:0
msgid "Messages"
-msgstr ""
+msgstr "訊息"
#. module: hr
#: view:hr.config.settings:0
msgid "Talent Management"
-msgstr ""
+msgstr "人才管理"
#. module: hr
#: help:hr.config.settings,module_hr_timesheet_sheet:0
msgid "This installs the module hr_timesheet_sheet."
-msgstr ""
+msgstr "此功能將安裝 hr_timesheet_sheet 模組。"
#. module: hr
#: view:hr.employee:0
msgid "Mobile:"
-msgstr ""
+msgstr "行動電話:"
#. module: hr
#: view:hr.employee:0
msgid "Position"
-msgstr ""
+msgstr "職位"
#. module: hr
#: help:hr.job,message_unread:0
msgid "If checked new messages require your attention."
-msgstr ""
+msgstr "當有新訊息時通知您。"
#. module: hr
#: field:hr.employee,color:0
msgid "Color Index"
-msgstr ""
+msgstr "顏色索引"
#. module: hr
#: model:process.transition,note:hr.process_transition_employeeuser0
@@ -236,7 +236,7 @@ msgstr ""
#. module: hr
#: field:hr.employee,image_medium:0
msgid "Medium-sized photo"
-msgstr ""
+msgstr "中等尺寸照片"
#. module: hr
#: field:hr.employee,identification_id:0
@@ -251,7 +251,7 @@ msgstr "女"
#. module: hr
#: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_new_config
msgid "Attendance"
-msgstr ""
+msgstr "出勤"
#. module: hr
#: field:hr.employee,work_phone:0
@@ -277,7 +277,7 @@ msgstr "辦公室位置"
#. module: hr
#: field:hr.job,message_follower_ids:0
msgid "Followers"
-msgstr ""
+msgstr "關注者"
#. module: hr
#: view:hr.employee:0
@@ -307,12 +307,12 @@ msgstr "出生日期"
#. module: hr
#: help:hr.job,no_of_recruitment:0
msgid "Number of new employees you expect to recruit."
-msgstr ""
+msgstr "您期望聘雇的新員工數量。"
#. module: hr
#: model:ir.actions.client,name:hr.action_client_hr_menu
msgid "Open HR Menu"
-msgstr ""
+msgstr "開啟人資選單"
#. module: hr
#: help:hr.job,message_summary:0
@@ -326,12 +326,12 @@ msgstr ""
msgid ""
"This installs the module account_analytic_analysis, which will install sales "
"management too."
-msgstr ""
+msgstr "此功能將安裝 account_analytic_analysis 模組,同時也安裝業務銷售管理模組。"
#. module: hr
#: view:board.board:0
msgid "Human Resources Dashboard"
-msgstr ""
+msgstr "人力資源儀表板"
#. module: hr
#: view:hr.employee:0
@@ -343,7 +343,7 @@ msgstr "工作"
#. module: hr
#: field:hr.job,no_of_employee:0
msgid "Current Number of Employees"
-msgstr ""
+msgstr "現有員工數目"
#. module: hr
#: field:hr.department,member_ids:0
@@ -353,22 +353,22 @@ msgstr "成員"
#. module: hr
#: model:ir.ui.menu,name:hr.menu_hr_configuration
msgid "Configuration"
-msgstr ""
+msgstr "組態設定"
#. module: hr
#: model:process.node,note:hr.process_node_employee0
msgid "Employee form and structure"
-msgstr ""
+msgstr "員工表單與架構"
#. module: hr
#: field:hr.config.settings,module_hr_expense:0
msgid "Manage employees expenses"
-msgstr ""
+msgstr "管理員工費用報支"
#. module: hr
#: view:hr.employee:0
msgid "Tel:"
-msgstr ""
+msgstr "電話:"
#. module: hr
#: selection:hr.employee,marital:0
@@ -430,7 +430,7 @@ msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_evaluation:0
msgid "This installs the module hr_evaluation."
-msgstr ""
+msgstr "此功能將安裝 hr_evaluation 模組。"
#. module: hr
#: constraint:hr.employee:0
@@ -440,12 +440,12 @@ msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_attendance:0
msgid "This installs the module hr_attendance."
-msgstr ""
+msgstr "此功能將安裝 hr_attendance 模組。"
#. module: hr
#: field:hr.employee,image_small:0
msgid "Smal-sized photo"
-msgstr ""
+msgstr "小尺吋照片"
#. module: hr
#: view:hr.employee.category:0
@@ -456,22 +456,22 @@ msgstr "員工分類"
#. module: hr
#: field:hr.employee,category_ids:0
msgid "Tags"
-msgstr ""
+msgstr "標籤"
#. module: hr
#: help:hr.config.settings,module_hr_contract:0
msgid "This installs the module hr_contract."
-msgstr ""
+msgstr "此功能將安裝 hr_contract 模組。"
#. module: hr
#: view:hr.employee:0
msgid "Related User"
-msgstr ""
+msgstr "相關使用者"
#. module: hr
#: view:hr.config.settings:0
msgid "or"
-msgstr ""
+msgstr "或"
#. module: hr
#: field:hr.employee.category,name:0
@@ -481,12 +481,12 @@ msgstr "分類"
#. module: hr
#: view:hr.job:0
msgid "Stop Recruitment"
-msgstr ""
+msgstr "停止招募"
#. module: hr
#: field:hr.config.settings,module_hr_attendance:0
msgid "Install attendances feature"
-msgstr ""
+msgstr "安裝出勤管理功能"
#. module: hr
#: help:hr.employee,bank_account_id:0
@@ -501,7 +501,7 @@ msgstr "備註"
#. module: hr
#: model:ir.actions.act_window,name:hr.open_view_employee_tree
msgid "Employees Structure"
-msgstr ""
+msgstr "員工架構"
#. module: hr
#: view:hr.employee:0
@@ -511,7 +511,7 @@ msgstr "聯絡資料"
#. module: hr
#: field:hr.config.settings,module_hr_holidays:0
msgid "Manage holidays, leaves and allocation requests"
-msgstr ""
+msgstr "管理假日、請假及排假。"
#. module: hr
#: field:hr.department,child_ids:0
@@ -528,7 +528,7 @@ msgstr "狀況"
#. module: hr
#: field:hr.employee,otherid:0
msgid "Other Id"
-msgstr ""
+msgstr "其他ID"
#. module: hr
#: model:process.process,name:hr.process_process_employeecontractprocess0
@@ -538,12 +538,12 @@ msgstr "僱傭合約"
#. module: hr
#: view:hr.config.settings:0
msgid "Contracts"
-msgstr ""
+msgstr "合約"
#. module: hr
#: help:hr.job,message_ids:0
msgid "Messages and communication history"
-msgstr ""
+msgstr "訊息及聯絡紀錄"
#. module: hr
#: field:hr.employee,ssnid:0
@@ -553,12 +553,12 @@ msgstr "社會保障號碼(美國)"
#. module: hr
#: field:hr.job,message_is_follower:0
msgid "Is a Follower"
-msgstr ""
+msgstr "為關注者"
#. module: hr
#: field:hr.config.settings,module_hr_recruitment:0
msgid "Manage the recruitment process"
-msgstr ""
+msgstr "管理招募流程"
#. module: hr
#: view:hr.employee:0
@@ -568,12 +568,12 @@ msgstr "活躍"
#. module: hr
#: view:hr.config.settings:0
msgid "Human Resources Management"
-msgstr ""
+msgstr "人力資源管理"
#. module: hr
#: view:hr.config.settings:0
msgid "Install your country's payroll"
-msgstr ""
+msgstr "安裝您國家的薪資模組"
#. module: hr
#: field:hr.employee,bank_account_id:0
@@ -588,7 +588,7 @@ msgstr "公司"
#. module: hr
#: field:hr.job,message_summary:0
msgid "Summary"
-msgstr ""
+msgstr "摘要"
#. module: hr
#: model:process.transition,note:hr.process_transition_contactofemployee0
@@ -615,12 +615,12 @@ msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "HR Settings"
-msgstr ""
+msgstr "人資設定"
#. module: hr
#: view:hr.employee:0
msgid "Citizenship & Other Info"
-msgstr ""
+msgstr "公民與其他資訊"
#. module: hr
#: constraint:hr.department:0
@@ -635,7 +635,7 @@ msgstr "辦公地址"
#. module: hr
#: view:hr.employee:0
msgid "Public Information"
-msgstr ""
+msgstr "公開資訊"
#. module: hr
#: field:hr.employee,marital:0
diff --git a/addons/hr/res_config.py b/addons/hr/res_config.py
index 147c4e4d5b9..94c029262ff 100644
--- a/addons/hr/res_config.py
+++ b/addons/hr/res_config.py
@@ -42,6 +42,8 @@ class hr_config_settings(osv.osv_memory):
help ="""This installs the module hr_contract."""),
'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation',
help ="""This installs the module hr_evaluation."""),
+ 'module_hr_gamification': fields.boolean('Drive engagement with challenges and badges',
+ help ="""This installs the module hr_gamification."""),
'module_account_analytic_analysis': fields.boolean('Allow invoicing based on timesheets (the sale application will be installed)',
help ="""This installs the module account_analytic_analysis, which will install sales management too."""),
'module_hr_payroll': fields.boolean('Manage payroll',
diff --git a/addons/hr/res_config_view.xml b/addons/hr/res_config_view.xml
index 632ceb56918..3dc1795139c 100644
--- a/addons/hr/res_config_view.xml
+++ b/addons/hr/res_config_view.xml
@@ -56,6 +56,10 @@
+
+
+
+
diff --git a/addons/hr/static/src/css/hr.css b/addons/hr/static/src/css/hr.css
index 951ec4d02e3..be21ca7e7a6 100644
--- a/addons/hr/static/src/css/hr.css
+++ b/addons/hr/static/src/css/hr.css
@@ -72,4 +72,4 @@
.openerp .oe_employee_vignette .oe_followers {
width: auto;
float: none;
-}
+}
\ No newline at end of file
diff --git a/addons/hr_evaluation/hr_evaluation.py b/addons/hr_evaluation/hr_evaluation.py
index 09c47b89153..462154e9e4a 100644
--- a/addons/hr_evaluation/hr_evaluation.py
+++ b/addons/hr_evaluation/hr_evaluation.py
@@ -101,6 +101,8 @@ Thanks,
class hr_employee(osv.osv):
_name = "hr.employee"
_inherit="hr.employee"
+
+
_columns = {
'evaluation_plan_id': fields.many2one('hr_evaluation.plan', 'Appraisal Plan'),
'evaluation_date': fields.date('Next Appraisal Date', help="The date of the next appraisal is computed by the appraisal plan's dates (first appraisal + periodicity)."),
@@ -123,6 +125,8 @@ class hr_employee(osv.osv):
obj_evaluation.button_plan_in_progress(cr, uid, [plan_id], context=context)
return True
+
+
class hr_evaluation(osv.osv):
_name = "hr_evaluation.evaluation"
_inherit = "mail.thread"
diff --git a/addons/hr_evaluation/security/ir.model.access.csv b/addons/hr_evaluation/security/ir.model.access.csv
index a304ee84c10..4be3d74b7d8 100644
--- a/addons/hr_evaluation/security/ir.model.access.csv
+++ b/addons/hr_evaluation/security/ir.model.access.csv
@@ -39,4 +39,4 @@ access_survey_response_hr_employee,survey.response.employee,survey.model_survey_
access_survey_question_column_heading_hr_employee,survey.question.column.heading.employee,survey.model_survey_question_column_heading,base.group_user,1,0,0,0
access_survey_response_line_hr_employee,survey.response.line.employee,survey.model_survey_response_line,base.group_user,1,1,1,0
access_survey_response_answer_hr_employee,survey.response.answer.hr.employee,survey.model_survey_response_answer,base.group_user,1,1,1,0
-access_survey_tbl_column_heading_hr_employee,survey.tbl.column.heading,survey.model_survey_tbl_column_heading,base.group_user,1,1,1,0
+access_survey_tbl_column_heading_hr_employee,survey.tbl.column.heading,survey.model_survey_tbl_column_heading,base.group_user,1,1,1,0
\ No newline at end of file
diff --git a/addons/hr_expense/i18n/zh_TW.po b/addons/hr_expense/i18n/zh_TW.po
index 29cf442efbc..506687e6e1c 100644
--- a/addons/hr_expense/i18n/zh_TW.po
+++ b/addons/hr_expense/i18n/zh_TW.po
@@ -7,57 +7,57 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.4\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
-"PO-Revision-Date: 2009-01-30 13:19+0000\n"
-"Last-Translator: <>\n"
+"PO-Revision-Date: 2013-12-27 04:48+0000\n"
+"Last-Translator: Andy Cheng \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2013-09-12 05:53+0000\n"
-"X-Generator: Launchpad (build 16761)\n"
+"X-Launchpad-Export-Date: 2013-12-27 05:22+0000\n"
+"X-Generator: Launchpad (build 16877)\n"
#. module: hr_expense
#: view:hr.expense.expense:0
#: model:process.node,name:hr_expense.process_node_confirmedexpenses0
msgid "Confirmed Expenses"
-msgstr ""
+msgstr "已確認費用報支"
#. module: hr_expense
#: model:ir.model,name:hr_expense.model_hr_expense_line
msgid "Expense Line"
-msgstr ""
+msgstr "費用報支項目"
#. module: hr_expense
#: model:process.node,note:hr_expense.process_node_reimbursement0
msgid "The accoutant reimburse the expenses"
-msgstr ""
+msgstr "會計師報支費用"
#. module: hr_expense
#: model:mail.message.subtype,description:hr_expense.mt_expense_approved
msgid "Expense approved"
-msgstr ""
+msgstr "費用報支已核准"
#. module: hr_expense
#: field:hr.expense.expense,date_confirm:0
#: field:hr.expense.report,date_confirm:0
msgid "Confirmation Date"
-msgstr ""
+msgstr "確認日期"
#. module: hr_expense
#: view:hr.expense.expense:0
#: view:hr.expense.report:0
msgid "Group By..."
-msgstr ""
+msgstr "分組..."
#. module: hr_expense
#: model:product.template,name:hr_expense.air_ticket_product_template
msgid "Air Ticket"
-msgstr ""
+msgstr "機票"
#. module: hr_expense
#: report:hr.expense:0
msgid "Validated By"
-msgstr ""
+msgstr "已完成審核"
#. module: hr_expense
#: view:hr.expense.expense:0
@@ -65,45 +65,45 @@ msgstr ""
#: view:hr.expense.report:0
#: field:hr.expense.report,department_id:0
msgid "Department"
-msgstr ""
+msgstr "部門"
#. module: hr_expense
#: view:hr.expense.expense:0
msgid "New Expense"
-msgstr ""
+msgstr "新增費用報支"
#. module: hr_expense
#: field:hr.expense.line,uom_id:0
#: view:product.product:0
msgid "Unit of Measure"
-msgstr ""
+msgstr "度量單位"
#. module: hr_expense
#: selection:hr.expense.report,month:0
msgid "March"
-msgstr ""
+msgstr "三月"
#. module: hr_expense
#: field:hr.expense.expense,message_unread:0
msgid "Unread Messages"
-msgstr ""
+msgstr "未讀訊息"
#. module: hr_expense
#: field:hr.expense.expense,company_id:0
#: view:hr.expense.report:0
#: field:hr.expense.report,company_id:0
msgid "Company"
-msgstr ""
+msgstr "公司"
#. module: hr_expense
#: view:hr.expense.expense:0
msgid "Set to Draft"
-msgstr ""
+msgstr "設為草稿"
#. module: hr_expense
#: view:hr.expense.expense:0
msgid "To Pay"
-msgstr ""
+msgstr "應付款"
#. module: hr_expense
#: code:addons/hr_expense/hr_expense.py:172
@@ -111,12 +111,12 @@ msgstr ""
msgid ""
"No expense journal found. Please make sure you have a journal with type "
"'purchase' configured."
-msgstr ""
+msgstr "未找到費用日記帳。請確認您是否有設置型態為「採購」的日記帳。"
#. module: hr_expense
#: model:ir.model,name:hr_expense.model_hr_expense_report
msgid "Expenses Statistics"
-msgstr ""
+msgstr "費用報支統計"
#. module: hr_expense
#: view:hr.expense.expense:0
@@ -127,7 +127,7 @@ msgstr ""
#: view:hr.expense.report:0
#: field:hr.expense.report,day:0
msgid "Day"
-msgstr ""
+msgstr "日"
#. module: hr_expense
#: help:hr.expense.expense,date_valid:0
@@ -139,12 +139,12 @@ msgstr ""
#. module: hr_expense
#: view:hr.expense.expense:0
msgid "Notes"
-msgstr ""
+msgstr "備註"
#. module: hr_expense
#: field:hr.expense.expense,message_ids:0
msgid "Messages"
-msgstr ""
+msgstr "訊息"
#. module: hr_expense
#: code:addons/hr_expense/hr_expense.py:172
@@ -154,28 +154,28 @@ msgstr ""
#: code:addons/hr_expense/hr_expense.py:353
#, python-format
msgid "Error!"
-msgstr ""
+msgstr "錯誤!"
#. module: hr_expense
#: model:mail.message.subtype,description:hr_expense.mt_expense_refused
msgid "Expense refused"
-msgstr ""
+msgstr "費用報支被拒"
#. module: hr_expense
#: model:ir.actions.act_window,name:hr_expense.hr_expense_product
#: view:product.product:0
msgid "Products"
-msgstr ""
+msgstr "產品"
#. module: hr_expense
#: view:hr.expense.report:0
msgid "Confirm Expenses"
-msgstr ""
+msgstr "確認費用報支"
#. module: hr_expense
#: selection:hr.expense.report,state:0
msgid "Cancelled"
-msgstr ""
+msgstr "已取消"
#. module: hr_expense
#: model:process.node,note:hr_expense.process_node_refused0
@@ -190,17 +190,17 @@ msgstr ""
#. module: hr_expense
#: selection:hr.expense.report,state:0
msgid "Waiting confirmation"
-msgstr ""
+msgstr "等待確認中"
#. module: hr_expense
#: selection:hr.expense.report,state:0
msgid "Accepted"
-msgstr ""
+msgstr "已接受"
#. module: hr_expense
#: field:hr.expense.line,ref:0
msgid "Reference"
-msgstr ""
+msgstr "參考"
#. module: hr_expense
#: report:hr.expense:0
@@ -228,7 +228,7 @@ msgstr ""
#: view:hr.expense.report:0
#: field:hr.expense.report,nbr:0
msgid "# of Lines"
-msgstr ""
+msgstr "項目數量"
#. module: hr_expense
#: help:hr.expense.expense,message_summary:0
@@ -241,33 +241,33 @@ msgstr ""
#: code:addons/hr_expense/hr_expense.py:453
#, python-format
msgid "Warning"
-msgstr ""
+msgstr "警告"
#. module: hr_expense
#: report:hr.expense:0
msgid "(Date and signature)"
-msgstr ""
+msgstr "(日期與簽名)"
#. module: hr_expense
#: report:hr.expense:0
msgid "Total:"
-msgstr ""
+msgstr "合計:"
#. module: hr_expense
#: model:process.transition,name:hr_expense.process_transition_refuseexpense0
msgid "Refuse expense"
-msgstr ""
+msgstr "拒絕費用報支"
#. module: hr_expense
#: field:hr.expense.report,price_average:0
msgid "Average Price"
-msgstr ""
+msgstr "平均價格"
#. module: hr_expense
#: view:hr.expense.expense:0
#: model:process.transition.action,name:hr_expense.process_transition_action_confirm0
msgid "Confirm"
-msgstr ""
+msgstr "確認"
#. module: hr_expense
#: model:process.node,note:hr_expense.process_node_supplierinvoice0
diff --git a/addons/hr_gamification/__init__.py b/addons/hr_gamification/__init__.py
new file mode 100644
index 00000000000..1896704c7fc
--- /dev/null
+++ b/addons/hr_gamification/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
+import models
+import wizard
\ No newline at end of file
diff --git a/addons/hr_gamification/__openerp__.py b/addons/hr_gamification/__openerp__.py
new file mode 100644
index 00000000000..c75a4454437
--- /dev/null
+++ b/addons/hr_gamification/__openerp__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+{
+ 'name': 'HR Gamification',
+ 'version': '1.0',
+ 'author': 'OpenERP SA',
+ 'category': 'hidden',
+ 'depends': ['gamification', 'hr'],
+ 'description': """Use the HR ressources for the gamification process.
+
+The HR officer can now manage challenges and badges.
+This allow the user to send badges to employees instead of simple users.
+Badge received are displayed on the user profile.
+""",
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'security/gamification_security.xml',
+ 'wizard/grant_badge.xml',
+ 'views/gamification.xml',
+ ],
+ 'js': ['static/src/js/gamification.js'],
+}
diff --git a/addons/hr_gamification/models/__init__.py b/addons/hr_gamification/models/__init__.py
new file mode 100644
index 00000000000..51cf01dfa09
--- /dev/null
+++ b/addons/hr_gamification/models/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
+import gamification
diff --git a/addons/hr_gamification/models/gamification.py b/addons/hr_gamification/models/gamification.py
new file mode 100644
index 00000000000..b2aa9039cb1
--- /dev/null
+++ b/addons/hr_gamification/models/gamification.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+
+
+class hr_gamification_badge_user(osv.Model):
+ """User having received a badge"""
+
+ _name = 'gamification.badge.user'
+ _inherit = ['gamification.badge.user']
+
+ _columns = {
+ 'employee_id': fields.many2one("hr.employee", string='Employee'),
+ }
+
+ def _check_employee_related_user(self, cr, uid, ids, context=None):
+ for badge_user in self.browse(cr, uid, ids, context=context):
+ if badge_user.user_id and badge_user.employee_id:
+ if badge_user.employee_id not in badge_user.user_id.employee_ids:
+ return False
+ return True
+
+ _constraints = [
+ (_check_employee_related_user, "The selected employee does not correspond to the selected user.", ['employee_id']),
+ ]
+
+
+class gamification_badge(osv.Model):
+ _name = 'gamification.badge'
+ _inherit = ['gamification.badge']
+
+ def get_granted_employees(self, cr, uid, badge_ids, context=None):
+ if context is None:
+ context = {}
+
+ employee_ids = []
+ badge_user_ids = self.pool.get('gamification.badge.user').search(cr, uid, [('badge_id', 'in', badge_ids), ('employee_id', '!=', False)], context=context)
+ for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context):
+ employee_ids.append(badge_user.employee_id.id)
+ # remove duplicates
+ employee_ids = list(set(employee_ids))
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Granted Employees',
+ 'view_mode': 'kanban,tree,form',
+ 'view_type': 'form',
+ 'res_model': 'hr.employee',
+ 'domain': [('id', 'in', employee_ids)]
+ }
+
+
+class hr_employee(osv.osv):
+ _name = "hr.employee"
+ _inherit = "hr.employee"
+
+ def _get_employee_goals(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the list of goals assigned to the employee"""
+ res = {}
+ for employee in self.browse(cr, uid, ids, context=context):
+ res[employee.id] = self.pool.get('gamification.goal').search(cr,uid,[('user_id', '=', employee.user_id.id), ('challenge_id.category', '=', 'hr')], context=context)
+ return res
+
+ def _get_employee_badges(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the list of badge_users assigned to the employee"""
+ res = {}
+ for employee in self.browse(cr, uid, ids, context=context):
+ res[employee.id] = self.pool.get('gamification.badge.user').search(cr, uid, [
+ '|',
+ ('employee_id', '=', employee.id),
+ '&',
+ ('employee_id', '=', False),
+ ('user_id', '=', employee.user_id.id)
+ ], context=context)
+ return res
+
+ def _has_badges(self, cr, uid, ids, field_name, arg, context=None):
+ """Return the list of badge_users assigned to the employee"""
+ res = {}
+ for employee in self.browse(cr, uid, ids, context=context):
+ employee_badge_ids = self.pool.get('gamification.badge.user').search(cr, uid, [
+ '|',
+ ('employee_id', '=', employee.id),
+ '&',
+ ('employee_id', '=', False),
+ ('user_id', '=', employee.user_id.id)
+ ], context=context)
+ res[employee.id] = len(employee_badge_ids) > 0
+ return res
+
+ _columns = {
+ 'goal_ids': fields.function(_get_employee_goals, type="one2many", obj='gamification.goal', string="Employee HR Goals"),
+ 'badge_ids': fields.function(_get_employee_badges, type="one2many", obj='gamification.badge.user', string="Employee Badges"),
+ 'has_badges': fields.function(_has_badges, type="boolean", string="Has Badges"),
+ }
diff --git a/addons/hr_gamification/security/gamification_security.xml b/addons/hr_gamification/security/gamification_security.xml
new file mode 100644
index 00000000000..e0f9a6cf918
--- /dev/null
+++ b/addons/hr_gamification/security/gamification_security.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ HR Officer can see any goal
+
+
+
+
+
+
+ [(1, '=', 1)]
+
+
+
+
diff --git a/addons/hr_gamification/security/ir.model.access.csv b/addons/hr_gamification/security/ir.model.access.csv
new file mode 100644
index 00000000000..b89db7cd104
--- /dev/null
+++ b/addons/hr_gamification/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+challenge_officer,"Challenge Officer",gamification.model_gamification_challenge,base.group_hr_user,1,1,1,1
+challenge_line_officer,"Challenge Line Officer",gamification.model_gamification_challenge_line,base.group_hr_user,1,1,1,1
+badge_officer,"Badge Officer",gamification.model_gamification_badge,base.group_hr_user,1,1,1,1
+badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,base.group_hr_user,1,1,1,1
\ No newline at end of file
diff --git a/addons/hr_gamification/static/src/js/gamification.js b/addons/hr_gamification/static/src/js/gamification.js
new file mode 100644
index 00000000000..a9126464a10
--- /dev/null
+++ b/addons/hr_gamification/static/src/js/gamification.js
@@ -0,0 +1,19 @@
+openerp.hr_gamification = function(instance) {
+ instance.web_kanban.KanbanRecord.include({
+ on_card_clicked: function() {
+ if (this.view.dataset.model === 'gamification.badge.user') {
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'gamification.badge',
+ view_mode: 'form',
+ view_type: 'form,kanban,tree',
+ views: [[false, 'form']],
+ res_id: this.record.badge_id.raw_value[0]
+ };
+ this.do_action(action);
+ } else {
+ this._super.apply(this, arguments);
+ }
+ }
+ });
+};
diff --git a/addons/hr_gamification/views/gamification.xml b/addons/hr_gamification/views/gamification.xml
new file mode 100644
index 00000000000..e2d45948b95
--- /dev/null
+++ b/addons/hr_gamification/views/gamification.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+ Badge Form
+ gamification.badge
+
+
+
+
+
+
+
+
+
+
+
+ hr.hr.employee.view.form
+ hr.employee
+
+
+
+
+
+
+ to reward this employee for a good action
+
+
+ Click to grant this employee his first badge
+
+ Badges are rewards of good work. Give them to people you believe deserve it.
+
+ Assign a list of goals to chosen users to evaluate them.
+ The challenge can use a period (weekly, monthly...) for automatic creation of goals.
+ The goals are created for the specified users or member of the group.
+
+
+
+
+
+ kanban
+
+
+
+
+
+ form
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_gamification/wizard/__init__.py b/addons/hr_gamification/wizard/__init__.py
new file mode 100644
index 00000000000..bb521f011bb
--- /dev/null
+++ b/addons/hr_gamification/wizard/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
+import grant_badge
\ No newline at end of file
diff --git a/addons/hr_gamification/wizard/grant_badge.py b/addons/hr_gamification/wizard/grant_badge.py
new file mode 100644
index 00000000000..b8a51044d4c
--- /dev/null
+++ b/addons/hr_gamification/wizard/grant_badge.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
+
+class hr_grant_badge_wizard(osv.TransientModel):
+ _name = 'gamification.badge.user.wizard'
+ _inherit = ['gamification.badge.user.wizard']
+
+ _columns = {
+ 'employee_id': fields.many2one("hr.employee", string='Employee', required=True),
+ 'user_id': fields.related("employee_id", "user_id",
+ type="many2one", relation="res.users",
+ store=True, string='User')
+ }
+
+ def action_grant_badge(self, cr, uid, ids, context=None):
+ """Wizard action for sending a badge to a chosen employee"""
+ if context is None:
+ context = {}
+
+ badge_user_obj = self.pool.get('gamification.badge.user')
+
+ for wiz in self.browse(cr, uid, ids, context=context):
+ if not wiz.user_id:
+ raise osv.except_osv(_('Warning!'), _('You can send badges only to employees linked to a user.'))
+
+ if uid == wiz.user_id.id:
+ raise osv.except_osv(_('Warning!'), _('You can not send a badge to yourself'))
+
+ values = {
+ 'user_id': wiz.user_id.id,
+ 'sender_id': uid,
+ 'badge_id': wiz.badge_id.id,
+ 'employee_id': wiz.employee_id.id,
+ 'comment': wiz.comment,
+ }
+
+ badge_user = badge_user_obj.create(cr, uid, values, context=context)
+ result = badge_user_obj._send_badge(cr, uid, [badge_user], context=context)
+ return result
\ No newline at end of file
diff --git a/addons/hr_gamification/wizard/grant_badge.xml b/addons/hr_gamification/wizard/grant_badge.xml
new file mode 100644
index 00000000000..6dcf2d2e7c4
--- /dev/null
+++ b/addons/hr_gamification/wizard/grant_badge.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+ Grant Badge Employee Form
+ gamification.badge.user.wizard
+
+
+
+
+
+
+
+
+
+
+
+ Reward Employee Badge Form
+ gamification.badge.user.wizard
+
+
+
+
+
+
+
+
+
diff --git a/addons/portal_gamification/__init__.py b/addons/portal_gamification/__init__.py
new file mode 100644
index 00000000000..484faca7b68
--- /dev/null
+++ b/addons/portal_gamification/__init__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
diff --git a/addons/portal_gamification/__openerp__.py b/addons/portal_gamification/__openerp__.py
new file mode 100644
index 00000000000..e20e54be6e3
--- /dev/null
+++ b/addons/portal_gamification/__openerp__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2013 OpenERP SA ().
+#
+# 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 .
+#
+##############################################################################
+
+
+{
+ 'name': 'Portal Gamification',
+ 'version': '1',
+ 'category': 'Tools',
+ 'complexity': 'easy',
+ 'description': """
+This module adds security rules for gamification to allow portal users to participate to challenges
+===================================================================================================
+ """,
+ 'author': 'OpenERP SA',
+ 'depends': ['gamification','portal'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'security/portal_security.xml',
+ ],
+ 'installable': True,
+ 'auto_install': True,
+ 'category': 'Hidden',
+}
+
diff --git a/addons/portal_gamification/security/ir.model.access.csv b/addons/portal_gamification/security/ir.model.access.csv
new file mode 100644
index 00000000000..4f21669cf55
--- /dev/null
+++ b/addons/portal_gamification/security/ir.model.access.csv
@@ -0,0 +1,7 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+goal_portal,"Goal Portal",gamification.model_gamification_goal,portal.group_portal,1,1,0,0
+goal_definition_portal,"Goal Definition Portal",gamification.model_gamification_goal_definition,portal.group_portal,1,0,0,0
+challenge_portal,"Goal Challenge Portal",gamification.model_gamification_challenge,portal.group_portal,1,0,0,0
+challenge_line_portal,"Challenge Line Portal",gamification.model_gamification_challenge_line,portal.group_portal,1,0,0,0
+badge_portal,"Badge Portal",gamification.model_gamification_badge,portal.group_portal,1,0,0,0
+badge_user_portal,"Badge-user Portal",gamification.model_gamification_badge_user,portal.group_portal,1,1,1,0
diff --git a/addons/portal_gamification/security/portal_security.xml b/addons/portal_gamification/security/portal_security.xml
new file mode 100644
index 00000000000..6b46fe96519
--- /dev/null
+++ b/addons/portal_gamification/security/portal_security.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/project_issue/i18n/he.po b/addons/project_issue/i18n/he.po
new file mode 100644
index 00000000000..a371ffffa71
--- /dev/null
+++ b/addons/project_issue/i18n/he.po
@@ -0,0 +1,984 @@
+# Hebrew translation for openobject-addons
+# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2013.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-12-21 17:04+0000\n"
+"PO-Revision-Date: 2013-12-26 23:15+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Hebrew \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2013-12-27 05:22+0000\n"
+"X-Generator: Launchpad (build 16877)\n"
+
+#. module: project_issue
+#: model:project.category,name:project_issue.project_issue_category_03
+msgid "Deadly bug"
+msgstr ""
+
+#. module: project_issue
+#: help:project.config.settings,fetchmail_issue:0
+msgid ""
+"Allows you to configure your incoming mail server, and create issues from "
+"incoming emails."
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue.report,delay_open:0
+msgid "Avg. Delay to Open"
+msgstr ""
+
+#. module: project_issue
+#: view:project.issue:0
+#: view:project.issue.report:0
+msgid "Group By..."
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,working_hours_open:0
+msgid "Working Hours to Open the Issue"
+msgstr ""
+
+#. module: project_issue
+#: model:mail.message.subtype,description:project_issue.mt_issue_started
+msgid "Issue started"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,date_open:0
+msgid "Opened"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue.report,opening_date:0
+msgid "Date of Opening"
+msgstr ""
+
+#. module: project_issue
+#: selection:project.issue.report,month:0
+msgid "March"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,progress:0
+msgid "Progress (%)"
+msgstr ""
+
+#. module: project_issue
+#: view:project.issue:0
+#: field:project.issue,message_unread:0
+msgid "Unread Messages"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,company_id:0
+#: view:project.issue.report:0
+#: field:project.issue.report,company_id:0
+msgid "Company"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,email_cc:0
+msgid "Watchers Emails"
+msgstr ""
+
+#. module: project_issue
+#: help:project.issue,kanban_state:0
+msgid ""
+"A Issue's kanban state indicates special situations affecting it:\n"
+" * Normal is the default situation\n"
+" * Blocked indicates something is preventing the progress of this issue\n"
+" * Ready for next stage indicates the issue is ready to be pulled to the "
+"next stage"
+msgstr ""
+
+#. module: project_issue
+#: help:project.issue,message_unread:0
+msgid "If checked new messages require your attention."
+msgstr ""
+
+#. module: project_issue
+#: help:account.analytic.account,use_issues:0
+msgid "Check this field if this project manages issues"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,day_open:0
+msgid "Days to Open"
+msgstr ""
+
+#. module: project_issue
+#: code:addons/project_issue/project_issue.py:479
+#, python-format
+msgid ""
+"You cannot escalate this issue.\n"
+"The relevant Project has not configured the Escalation Project!"
+msgstr ""
+
+#. module: project_issue
+#: constraint:project.project:0
+msgid "Error! You cannot assign escalation to the same project!"
+msgstr ""
+
+#. module: project_issue
+#: selection:project.issue,priority:0
+#: selection:project.issue.report,priority:0
+msgid "Highest"
+msgstr ""
+
+#. module: project_issue
+#: help:project.issue,inactivity_days:0
+msgid "Difference in days between last action and current date"
+msgstr ""
+
+#. module: project_issue
+#: view:project.issue.report:0
+#: field:project.issue.report,day:0
+msgid "Day"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,days_since_creation:0
+msgid "Days since creation date"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,task_id:0
+#: view:project.issue.report:0
+#: field:project.issue.report,task_id:0
+msgid "Task"
+msgstr ""
+
+#. module: project_issue
+#: model:mail.message.subtype,name:project_issue.mt_project_issue_stage
+msgid "Issue Stage Changed"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,message_ids:0
+msgid "Messages"
+msgstr ""
+
+#. module: project_issue
+#: field:project.issue,inactivity_days:0
+msgid "Days since last action"
+msgstr ""
+
+#. module: project_issue
+#: model:ir.model,name:project_issue.model_project_project
+#: view:project.issue:0
+#: field:project.issue,project_id:0
+#: view:project.issue.report:0
+#: field:project.issue.report,project_id:0
+msgid "Project"
+msgstr ""
+
+#. module: project_issue
+#: model:ir.actions.act_window,help:project_issue.project_issue_categ_act0
+msgid ""
+"
\n"
+" Click to report a new issue.\n"
+"
\n"
+" The OpenERP issues tacker allows you to efficiantly manage "
+"things\n"
+" like internal requests, software development bugs, customer\n"
+" complaints, project troubles, material breakdowns, etc.\n"
+"