\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2014-03-19 05:52+0000\n"
+"X-Generator: Launchpad (build 16963)\n"
+
+#. module: hr
+#: model:process.node,name:hr.process_node_openerpuser0
+msgid "Openerp user"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_timesheet_sheet:0
+msgid "Allow timesheets validation by managers"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,requirements:0
+msgid "Requirements"
+msgstr "አስፈላጊ"
+
+#. module: hr
+#: model:process.transition,name:hr.process_transition_contactofemployee0
+msgid "Link the employee to information"
+msgstr "የሰራተኞች መረጃ ማገናኘት"
+
+#. module: hr
+#: field:hr.employee,sinid:0
+msgid "SIN No"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,name:hr.open_board_hr
+#: model:ir.ui.menu,name:hr.menu_hr_dashboard
+#: model:ir.ui.menu,name:hr.menu_hr_main
+#: model:ir.ui.menu,name:hr.menu_hr_reporting
+#: model:ir.ui.menu,name:hr.menu_hr_root
+#: model:ir.ui.menu,name:hr.menu_human_resources_configuration
+msgid "Human Resources"
+msgstr "የሰው ሀይል አስተዳደር"
+
+#. module: hr
+#: help:hr.employee,image_medium:0
+msgid ""
+"Medium-sized photo of the employee. It is automatically resized as a "
+"128x128px image, with aspect ratio preserved. Use this field in form views "
+"or some kanban views."
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Time Tracking"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+#: view:hr.job:0
+msgid "Group By..."
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,name:hr.view_department_form_installer
+msgid "Create Your Departments"
+msgstr ""
+
+#. module: hr
+#: help:hr.job,no_of_employee:0
+msgid "Number of employees currently occupying this job position."
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_evaluation:0
+msgid "Organize employees periodic evaluation"
+msgstr ""
+
+#. module: hr
+#: view:hr.department:0
+#: view:hr.employee:0
+#: field:hr.employee,department_id:0
+#: view:hr.job:0
+#: field:hr.job,department_id:0
+#: model:ir.model,name:hr.model_hr_department
+msgid "Department"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,work_email:0
+msgid "Work Email"
+msgstr ""
+
+#. module: hr
+#: help:hr.employee,image:0
+msgid ""
+"This field holds the image used as photo for the employee, limited to "
+"1024x1024px."
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_holidays:0
+msgid "This installs the module hr_holidays."
+msgstr ""
+
+#. module: hr
+#: view:hr.job:0
+msgid "Jobs"
+msgstr ""
+
+#. module: hr
+#: view:hr.job:0
+msgid "In Recruitment"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,message_unread:0
+msgid "Unread Messages"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,company_id:0
+#: view:hr.employee:0
+#: view:hr.job:0
+#: field:hr.job,company_id:0
+msgid "Company"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,no_of_recruitment:0
+msgid "Expected in Recruitment"
+msgstr ""
+
+#. module: hr
+#: field:res.users,employee_ids:0
+msgid "Related employees"
+msgstr ""
+
+#. module: hr
+#: constraint:hr.employee.category:0
+msgid "Error! You cannot create recursive Categories."
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_recruitment:0
+msgid "This installs the module hr_recruitment."
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Birth"
+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 ""
+
+#. module: hr
+#: view:hr.job:0
+msgid "Launch Recruitement"
+msgstr ""
+
+#. module: hr
+#: model:process.transition,name:hr.process_transition_employeeuser0
+msgid "Link a user to an employee"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,parent_id:0
+msgid "Parent Department"
+msgstr ""
+
+#. module: hr
+#: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_config
+msgid "Leaves"
+msgstr ""
+
+#. module: hr
+#: selection:hr.employee,marital:0
+msgid "Married"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,message_ids:0
+msgid "Messages"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Talent Management"
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_timesheet_sheet:0
+msgid "This installs the module hr_timesheet_sheet."
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Mobile:"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Position"
+msgstr ""
+
+#. module: hr
+#: help:hr.job,message_unread:0
+msgid "If checked new messages require your attention."
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,color:0
+msgid "Color Index"
+msgstr ""
+
+#. module: hr
+#: model:process.transition,note:hr.process_transition_employeeuser0
+msgid ""
+"The Related user field on the Employee form allows to link the OpenERP user "
+"(and her rights) to the employee."
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,image_medium:0
+msgid "Medium-sized photo"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,identification_id:0
+msgid "Identification No"
+msgstr ""
+
+#. module: hr
+#: selection:hr.employee,gender:0
+msgid "Female"
+msgstr ""
+
+#. module: hr
+#: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_new_config
+msgid "Attendance"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,work_phone:0
+msgid "Work Phone"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee.category,child_ids:0
+msgid "Child Categories"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,description:0
+#: model:ir.model,name:hr.model_hr_job
+msgid "Job Description"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,work_location:0
+msgid "Office Location"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,message_follower_ids:0
+msgid "Followers"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+#: model:ir.model,name:hr.model_hr_employee
+#: model:process.node,name:hr.process_node_employee0
+msgid "Employee"
+msgstr ""
+
+#. module: hr
+#: model:process.node,note:hr.process_node_employeecontact0
+msgid "Other information"
+msgstr ""
+
+#. module: hr
+#: help:hr.employee,image_small:0
+msgid ""
+"Small-sized photo of the employee. It is automatically resized as a 64x64px "
+"image, with aspect ratio preserved. Use this field anywhere a small image is "
+"required."
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,birthday:0
+msgid "Date of Birth"
+msgstr ""
+
+#. module: hr
+#: help:hr.job,no_of_recruitment:0
+msgid "Number of new employees you expect to recruit."
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.client,name:hr.action_client_hr_menu
+msgid "Open HR Menu"
+msgstr ""
+
+#. module: hr
+#: help:hr.job,message_summary:0
+msgid ""
+"Holds the Chatter summary (number of messages, ...). This summary is "
+"directly in html format in order to be inserted in kanban views."
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_account_analytic_analysis:0
+msgid ""
+"This installs the module account_analytic_analysis, which will install sales "
+"management too."
+msgstr ""
+
+#. module: hr
+#: view:board.board:0
+msgid "Human Resources Dashboard"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+#: field:hr.employee,job_id:0
+#: view:hr.job:0
+msgid "Job"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,no_of_employee:0
+msgid "Current Number of Employees"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,member_ids:0
+msgid "Members"
+msgstr ""
+
+#. module: hr
+#: model:ir.ui.menu,name:hr.menu_hr_configuration
+msgid "Configuration"
+msgstr ""
+
+#. module: hr
+#: model:process.node,note:hr.process_node_employee0
+msgid "Employee form and structure"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_expense:0
+msgid "Manage employees expenses"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Tel:"
+msgstr ""
+
+#. module: hr
+#: selection:hr.employee,marital:0
+msgid "Divorced"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee.category,parent_id:0
+msgid "Parent Category"
+msgstr ""
+
+#. module: hr
+#: view:hr.department:0
+#: model:ir.actions.act_window,name:hr.open_module_tree_department
+#: model:ir.ui.menu,name:hr.menu_hr_department_tree
+msgid "Departments"
+msgstr ""
+
+#. module: hr
+#: model:process.node,name:hr.process_node_employeecontact0
+msgid "Employee Contact"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,help:hr.action_hr_job
+msgid ""
+"\n"
+" Click to define a new job position.\n"
+"
\n"
+" Job Positions are used to define jobs and their "
+"requirements.\n"
+" You can keep track of the number of employees you have per "
+"job\n"
+" position and follow the evolution according to what you "
+"planned\n"
+" for the future.\n"
+"
\n"
+" You can attach a survey to a job position. It will be used "
+"in\n"
+" the recruitment process to evaluate the applicants for this "
+"job\n"
+" position.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: hr
+#: selection:hr.employee,gender:0
+msgid "Male"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid ""
+"$('.oe_employee_picture').load(function() { if($(this).width() > "
+"$(this).height()) { $(this).addClass('oe_employee_picture_wide') } });"
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_evaluation:0
+msgid "This installs the module hr_evaluation."
+msgstr ""
+
+#. module: hr
+#: constraint:hr.employee:0
+msgid "Error! You cannot create recursive hierarchy of Employee(s)."
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_attendance:0
+msgid "This installs the module hr_attendance."
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,image_small:0
+msgid "Smal-sized photo"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee.category:0
+#: model:ir.model,name:hr.model_hr_employee_category
+msgid "Employee Category"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,category_ids:0
+msgid "Tags"
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_contract:0
+msgid "This installs the module hr_contract."
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Related User"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "or"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee.category,name:0
+msgid "Category"
+msgstr ""
+
+#. module: hr
+#: view:hr.job:0
+msgid "Stop Recruitment"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_attendance:0
+msgid "Install attendances feature"
+msgstr ""
+
+#. module: hr
+#: help:hr.employee,bank_account_id:0
+msgid "Employee bank salary account"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,note:0
+msgid "Note"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,name:hr.open_view_employee_tree
+msgid "Employees Structure"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Contact Information"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_holidays:0
+msgid "Manage holidays, leaves and allocation requests"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,child_ids:0
+msgid "Child Departments"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+#: view:hr.job:0
+#: field:hr.job,state:0
+msgid "Status"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,otherid:0
+msgid "Other Id"
+msgstr ""
+
+#. module: hr
+#: model:process.process,name:hr.process_process_employeecontractprocess0
+msgid "Employee Contract"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Contracts"
+msgstr ""
+
+#. module: hr
+#: help:hr.job,message_ids:0
+msgid "Messages and communication history"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,ssnid:0
+msgid "SSN No"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,message_is_follower:0
+msgid "Is a Follower"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_recruitment:0
+msgid "Manage the recruitment process"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Active"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Human Resources Management"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Install your country's payroll"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,bank_account_id:0
+msgid "Bank Account Number"
+msgstr ""
+
+#. module: hr
+#: view:hr.department:0
+msgid "Companies"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,message_summary:0
+msgid "Summary"
+msgstr ""
+
+#. module: hr
+#: model:process.transition,note:hr.process_transition_contactofemployee0
+msgid ""
+"In the Employee form, there are different kind of information like Contact "
+"information."
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,help:hr.open_view_employee_list_my
+msgid ""
+"\n"
+" Click to add a new employee.\n"
+"
\n"
+" With just a quick glance on the OpenERP employee screen, "
+"you\n"
+" can easily find all the information you need for each "
+"person;\n"
+" contact data, job position, availability, etc.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "HR Settings"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Citizenship & Other Info"
+msgstr ""
+
+#. module: hr
+#: constraint:hr.department:0
+msgid "Error! You cannot create recursive departments."
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,address_id:0
+msgid "Working Address"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Public Information"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,marital:0
+msgid "Marital Status"
+msgstr ""
+
+#. module: hr
+#: model:ir.model,name:hr.model_ir_actions_act_window
+msgid "ir.actions.act_window"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,last_login:0
+msgid "Latest Connection"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,image:0
+msgid "Photo"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Cancel"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,help:hr.open_module_tree_department
+msgid ""
+"\n"
+" Click to create a department.\n"
+"
\n"
+" OpenERP's department structure is used to manage all "
+"documents\n"
+" related to employees by departments: expenses, timesheets,\n"
+" leaves and holidays, recruitments, etc.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_timesheet:0
+msgid "This installs the module hr_timesheet."
+msgstr ""
+
+#. module: hr
+#: help:hr.job,expected_employees:0
+msgid ""
+"Expected number of employees for this job position after new recruitment."
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,help:hr.view_department_form_installer
+msgid ""
+"\n"
+" Click to define a new department.\n"
+"
\n"
+" Your departments structure is used to manage all documents\n"
+" related to employees by departments: expenses and "
+"timesheets,\n"
+" leaves and holidays, recruitments, etc.\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+msgid "Personal Information"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,city:0
+msgid "City"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,passport_id:0
+msgid "Passport No"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,mobile_phone:0
+msgid "Work Mobile"
+msgstr ""
+
+#. module: hr
+#: selection:hr.job,state:0
+msgid "Recruitement in Progress"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_account_analytic_analysis:0
+msgid ""
+"Allow invoicing based on timesheets (the sale application will be installed)"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee.category:0
+msgid "Employees Categories"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,address_home_id:0
+msgid "Home Address"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_timesheet:0
+msgid "Manage timesheets"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,name:hr.open_payroll_modules
+msgid "Payroll"
+msgstr ""
+
+#. module: hr
+#: selection:hr.employee,marital:0
+msgid "Single"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,name:0
+msgid "Job Name"
+msgstr ""
+
+#. module: hr
+#: view:hr.job:0
+msgid "In Position"
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_payroll:0
+msgid "This installs the module hr_payroll."
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_contract:0
+msgid "Record contracts per employee"
+msgstr ""
+
+#. module: hr
+#: view:hr.department:0
+msgid "department"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,country_id:0
+msgid "Nationality"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Additional Features"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,notes:0
+msgid "Notes"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,name:hr.action2
+msgid "Subordinate Hierarchy"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,resource_id:0
+msgid "Resource"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,complete_name:0
+#: field:hr.employee,name_related:0
+#: field:hr.employee.category,complete_name:0
+msgid "Name"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,gender:0
+msgid "Gender"
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+#: field:hr.employee.category,employee_ids:0
+#: field:hr.job,employee_ids:0
+#: model:ir.actions.act_window,name:hr.hr_employee_normal_action_tree
+#: model:ir.actions.act_window,name:hr.open_view_employee_list
+#: model:ir.actions.act_window,name:hr.open_view_employee_list_my
+#: model:ir.ui.menu,name:hr.menu_open_view_employee_list_my
+msgid "Employees"
+msgstr ""
+
+#. module: hr
+#: help:hr.employee,sinid:0
+msgid "Social Insurance Number"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,name:0
+msgid "Department Name"
+msgstr ""
+
+#. module: hr
+#: model:ir.ui.menu,name:hr.menu_hr_reporting_timesheet
+msgid "Reports"
+msgstr ""
+
+#. module: hr
+#: field:hr.config.settings,module_hr_payroll:0
+msgid "Manage payroll"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+#: model:ir.actions.act_window,name:hr.action_human_resources_configuration
+msgid "Configure Human Resources"
+msgstr ""
+
+#. module: hr
+#: selection:hr.job,state:0
+msgid "No Recruitment"
+msgstr ""
+
+#. module: hr
+#: help:hr.employee,ssnid:0
+msgid "Social Security Number"
+msgstr ""
+
+#. module: hr
+#: model:process.node,note:hr.process_node_openerpuser0
+msgid "Creation of a OpenERP user"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,login:0
+msgid "Login"
+msgstr ""
+
+#. module: hr
+#: field:hr.job,expected_employees:0
+msgid "Total Forecasted Employees"
+msgstr ""
+
+#. module: hr
+#: help:hr.job,state:0
+msgid ""
+"By default 'In position', set it to 'In Recruitment' if recruitment process "
+"is going on for this job position."
+msgstr ""
+
+#. module: hr
+#: model:ir.model,name:hr.model_res_users
+msgid "Users"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,name:hr.action_hr_job
+#: model:ir.ui.menu,name:hr.menu_hr_job
+msgid "Job Positions"
+msgstr ""
+
+#. module: hr
+#: model:ir.actions.act_window,help:hr.open_board_hr
+msgid ""
+"\n"
+"
\n"
+" Human Resources dashboard is empty.\n"
+"
\n"
+" To add your first report into this dashboard, go to any\n"
+" menu, switch to list or graph view, and click 'Add "
+"to\n"
+" Dashboard' in the extended search options.\n"
+"
\n"
+" You can filter and group data before inserting into the\n"
+" dashboard using the search options.\n"
+"
\n"
+"
\n"
+" "
+msgstr ""
+
+#. module: hr
+#: view:hr.employee:0
+#: field:hr.employee,coach_id:0
+msgid "Coach"
+msgstr ""
+
+#. module: hr
+#: sql_constraint:hr.job:0
+msgid "The name of the job position must be unique per company!"
+msgstr ""
+
+#. module: hr
+#: help:hr.config.settings,module_hr_expense:0
+msgid "This installs the module hr_expense."
+msgstr ""
+
+#. module: hr
+#: model:ir.model,name:hr.model_hr_config_settings
+msgid "hr.config.settings"
+msgstr ""
+
+#. module: hr
+#: field:hr.department,manager_id:0
+#: view:hr.employee:0
+#: field:hr.employee,parent_id:0
+msgid "Manager"
+msgstr ""
+
+#. module: hr
+#: selection:hr.employee,marital:0
+msgid "Widower"
+msgstr ""
+
+#. module: hr
+#: field:hr.employee,child_ids:0
+msgid "Subordinates"
+msgstr ""
+
+#. module: hr
+#: view:hr.config.settings:0
+msgid "Apply"
+msgstr ""
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index b8bf703f604..17dd4b57cd0 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -76,8 +76,8 @@ openerp.mail = function (session) {
*/
get_text2html: function (text) {
return text
- .replace(/[\n\r]/g,'
')
.replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ')
+ .replace(/[\n\r]/g,'
')
},
/* Returns the complete domain with "&"
diff --git a/addons/mrp/report/bom_structure.py b/addons/mrp/report/bom_structure.py
index 9af4e91b8ce..98949ed75dc 100644
--- a/addons/mrp/report/bom_structure.py
+++ b/addons/mrp/report/bom_structure.py
@@ -19,24 +19,22 @@
#
##############################################################################
-from openerp.addons.web import http
-from openerp.addons.web.http import request
+from openerp.osv import osv
-class bom_structure(http.Controller):
+class bom_structure(osv.AbstractModel):
+ _name = 'report.mrp.report_mrpbomstructure'
- @http.route(['/report/mrp.report_mrpbomstructure/'], type='http', auth='user', website=True, multilang=True)
- def report_mrpbomstructure(self, docids):
- ids = [int(i) for i in docids.split(',')]
- ids = list(set(ids))
- report_obj = request.registry['mrp.bom']
- docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
+ def render_html(self, cr, uid, ids, data=None, context=None):
+ mrpbom_obj = self.pool['mrp.bom']
+ report_obj = self.pool['report']
+ docs = mrpbom_obj.browse(cr, uid, ids, context=context)
docargs = {
'docs': docs,
'get_children': self.get_children,
}
- return request.registry['report'].render(request.cr, request.uid, [], 'mrp.report_mrpbomstructure', docargs)
+ return report_obj.render(cr, uid, [], 'mrp.report_mrpbomstructure', docargs, context=context)
def get_children(self, object, level=0):
result = []
diff --git a/addons/point_of_sale/security/point_of_sale_security.xml b/addons/point_of_sale/security/point_of_sale_security.xml
index 54572ee3eb9..5e98bcba49e 100644
--- a/addons/point_of_sale/security/point_of_sale_security.xml
+++ b/addons/point_of_sale/security/point_of_sale_security.xml
@@ -19,5 +19,11 @@
[('company_id', '=', user.company_id.id)]
+
+ Point Of Sale Config
+
+
+ [('warehouse_id.company_id','child_of',[user.company_id.id])]
+
diff --git a/addons/point_of_sale/wizard/pos_session_opening.xml b/addons/point_of_sale/wizard/pos_session_opening.xml
index 5aa3f8d4764..2f3f244c318 100644
--- a/addons/point_of_sale/wizard/pos_session_opening.xml
+++ b/addons/point_of_sale/wizard/pos_session_opening.xml
@@ -10,7 +10,7 @@
diff --git a/addons/portal/mail_mail.py b/addons/portal/mail_mail.py
index 18eb15361a3..0fe2081fa45 100644
--- a/addons/portal/mail_mail.py
+++ b/addons/portal/mail_mail.py
@@ -39,7 +39,7 @@ class mail_mail(osv.Model):
if partner and not partner.user_ids:
contex_signup = dict(context, signup_valid=True)
signup_url = partner_obj._get_signup_url_for_action(cr, SUPERUSER_ID, [partner.id],
- action='login', model=mail.model, res_id=mail.res_id,
+ model=mail.model, res_id=mail.res_id,
context=contex_signup)[partner.id]
return _("""""") % signup_url
else:
diff --git a/addons/portal/portal.py b/addons/portal/portal.py
index bbab8b16cd3..d94e541eb03 100644
--- a/addons/portal/portal.py
+++ b/addons/portal/portal.py
@@ -30,10 +30,3 @@ class portal(osv.osv):
_columns = {
'is_portal': fields.boolean('Portal', help="If checked, this group is usable as a portal."),
}
-
-class res_users(osv.Model):
- _inherit = 'res.users'
- def _signup_create_user(self, cr, uid, values, context=None):
- values['share'] = True
- return super(res_users, self)._signup_create_user(cr, uid, values, context=context)
-
diff --git a/addons/portal/portal_data.xml b/addons/portal/portal_data.xml
index 35dee070c71..273d1a8c463 100644
--- a/addons/portal/portal_data.xml
+++ b/addons/portal/portal_data.xml
@@ -6,9 +6,6 @@
-
-
-
diff --git a/addons/portal/portal_demo.xml b/addons/portal/portal_demo.xml
index 3abc9c8cef5..f6e46708bbc 100644
--- a/addons/portal/portal_demo.xml
+++ b/addons/portal/portal_demo.xml
@@ -22,7 +22,6 @@
Mr Demo Portal
-
diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py
index 66cb0958774..2c4d7ecf966 100644
--- a/addons/portal/tests/test_portal.py
+++ b/addons/portal/tests/test_portal.py
@@ -134,7 +134,8 @@ class test_portal(TestMail):
'invite: subject of invitation email is incorrect')
self.assertIn('Administrator invited you to follow Discussion group document: Pigs', sent_email.get('body'),
'invite: body of invitation email is incorrect')
- self.assertTrue(partner_carine.signup_url in sent_email.get('body'),
+ invite_url = partner_carine._get_signup_url_for_action(model='mail.group', res_id=self.group_pigs_id)[partner_carine.id]
+ self.assertTrue(invite_url in sent_email.get('body'),
'invite: body of invitation email does not contain signup url')
def test_20_notification_url(self):
diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py
index 3973bf6b131..3422a41baff 100644
--- a/addons/portal/wizard/portal_wizard.py
+++ b/addons/portal/wizard/portal_wizard.py
@@ -209,7 +209,6 @@ class wizard_user(osv.osv_memory):
'login': extract_email(wizard_user.email),
'partner_id': wizard_user.partner_id.id,
'groups_id': [(6, 0, [])],
- 'share': True,
}
user_id = res_users.create(cr, uid, values, context=create_context)
return res_users.browse(cr, uid, user_id, context)
diff --git a/addons/product_margin/i18n/am.po b/addons/product_margin/i18n/am.po
new file mode 100644
index 00000000000..c9eea96959e
--- /dev/null
+++ b/addons/product_margin/i18n/am.po
@@ -0,0 +1,283 @@
+# Amharic translation for openobject-addons
+# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-12-21 17:06+0000\n"
+"PO-Revision-Date: 2014-03-18 08:01+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Amharic \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2014-03-19 05:52+0000\n"
+"X-Generator: Launchpad (build 16963)\n"
+
+#. module: product_margin
+#: view:product.product:0
+#: field:product.product,turnover:0
+msgid "Turnover"
+msgstr "ተመላሽ"
+
+#. module: product_margin
+#: field:product.product,expected_margin_rate:0
+msgid "Expected Margin (%)"
+msgstr ""
+
+#. module: product_margin
+#: field:product.margin,from_date:0
+msgid "From"
+msgstr "ከ"
+
+#. module: product_margin
+#: help:product.product,total_cost:0
+msgid ""
+"Sum of Multiplication of Invoice price and quantity of Supplier Invoices "
+msgstr ""
+
+#. module: product_margin
+#: field:product.margin,to_date:0
+msgid "To"
+msgstr "ለ"
+
+#. module: product_margin
+#: help:product.product,total_margin:0
+msgid "Turnover - Standard price"
+msgstr "የተመላሽ መደበኛ ዋጋ"
+
+#. module: product_margin
+#: field:product.product,total_margin_rate:0
+msgid "Total Margin Rate(%)"
+msgstr ""
+
+#. module: product_margin
+#: selection:product.margin,invoice_state:0
+#: selection:product.product,invoice_state:0
+msgid "Draft, Open and Paid"
+msgstr "ያልተከፈለና የተከፈለ ደረሰኞች"
+
+#. module: product_margin
+#: code:addons/product_margin/wizard/product_margin.py:73
+#: model:ir.actions.act_window,name:product_margin.product_margin_act_window
+#: model:ir.ui.menu,name:product_margin.menu_action_product_margin
+#: view:product.product:0
+#, python-format
+msgid "Product Margins"
+msgstr "የእቃው አይነት በአንድ መጠን ሲጨምር"
+
+#. module: product_margin
+#: field:product.product,purchase_avg_price:0
+#: field:product.product,sale_avg_price:0
+msgid "Avg. Unit Price"
+msgstr "የእቃዎች መካከለኛ ዋጋ"
+
+#. module: product_margin
+#: field:product.product,sale_num_invoiced:0
+msgid "# Invoiced in Sale"
+msgstr ""
+
+#. module: product_margin
+#: view:product.product:0
+msgid "Catalog Price"
+msgstr "ቅናሽ ዋጋ"
+
+#. module: product_margin
+#: selection:product.margin,invoice_state:0
+#: selection:product.product,invoice_state:0
+msgid "Paid"
+msgstr "ተከፈል"
+
+#. module: product_margin
+#: view:product.product:0
+#: field:product.product,sales_gap:0
+msgid "Sales Gap"
+msgstr "የሽያጭ ክፍተት"
+
+#. module: product_margin
+#: help:product.product,sales_gap:0
+msgid "Expected Sale - Turn Over"
+msgstr "ሊሸጥ የሚችል እቃ"
+
+#. module: product_margin
+#: field:product.product,sale_expected:0
+msgid "Expected Sale"
+msgstr "ሊሸጥ የሚችል እቃ"
+
+#. module: product_margin
+#: view:product.product:0
+msgid "Standard Price"
+msgstr "የእቃው መደበኛ ዋጋ"
+
+#. module: product_margin
+#: help:product.product,purchase_num_invoiced:0
+msgid "Sum of Quantity in Supplier Invoices"
+msgstr ""
+
+#. module: product_margin
+#: field:product.product,date_to:0
+msgid "Margin Date To"
+msgstr "የእቃው መጠን የጨመረበት ቀን"
+
+#. module: product_margin
+#: view:product.product:0
+msgid "Analysis Criteria"
+msgstr "የመመዘኛ መስፈርት"
+
+#. module: product_margin
+#: view:product.product:0
+#: field:product.product,total_cost:0
+msgid "Total Cost"
+msgstr "አጠቃላይ ዋጋ"
+
+#. module: product_margin
+#: help:product.product,normal_cost:0
+msgid "Sum of Multiplication of Cost price and quantity of Supplier Invoices"
+msgstr ""
+
+#. module: product_margin
+#: field:product.product,expected_margin:0
+msgid "Expected Margin"
+msgstr "የሚጠበቅ ጭማሪ"
+
+#. module: product_margin
+#: view:product.product:0
+msgid "#Purchased"
+msgstr ""
+
+#. module: product_margin
+#: help:product.product,expected_margin_rate:0
+msgid "Expected margin * 100 / Expected Sale"
+msgstr ""
+
+#. module: product_margin
+#: help:product.product,sale_avg_price:0
+msgid "Avg. Price in Customer Invoices."
+msgstr "የመካከለኛ ዋጋ ለገዢዎች"
+
+#. module: product_margin
+#: help:product.product,purchase_avg_price:0
+msgid "Avg. Price in Supplier Invoices "
+msgstr "የመካከለኛ ዋጋ አቅራቢዎች "
+
+#. module: product_margin
+#: field:product.margin,invoice_state:0
+#: field:product.product,invoice_state:0
+msgid "Invoice State"
+msgstr ""
+
+#. module: product_margin
+#: help:product.product,purchase_gap:0
+msgid "Normal Cost - Total Cost"
+msgstr ""
+
+#. module: product_margin
+#: help:product.product,sale_expected:0
+msgid ""
+"Sum of Multiplication of Sale Catalog price and quantity of Customer Invoices"
+msgstr ""
+
+#. module: product_margin
+#: field:product.product,total_margin:0
+msgid "Total Margin"
+msgstr "የሁሉም ዋጋ በአንድ መጠን ሲጨምር"
+
+#. module: product_margin
+#: field:product.product,date_from:0
+msgid "Margin Date From"
+msgstr "እቃው ከጨመረበት ቀን ጀምሮ"
+
+#. module: product_margin
+#: help:product.product,turnover:0
+msgid ""
+"Sum of Multiplication of Invoice price and quantity of Customer Invoices"
+msgstr ""
+
+#. module: product_margin
+#: field:product.product,normal_cost:0
+msgid "Normal Cost"
+msgstr "መደበኛ ዋጋ"
+
+#. module: product_margin
+#: view:product.product:0
+msgid "Purchases"
+msgstr "ግዢዎች"
+
+#. module: product_margin
+#: field:product.product,purchase_num_invoiced:0
+msgid "# Invoiced in Purchase"
+msgstr ""
+
+#. module: product_margin
+#: help:product.product,expected_margin:0
+msgid "Expected Sale - Normal Cost"
+msgstr ""
+
+#. module: product_margin
+#: view:product.margin:0
+msgid "Properties categories"
+msgstr "በአይነታቸው መከፍፈል"
+
+#. module: product_margin
+#: help:product.product,total_margin_rate:0
+msgid "Total margin * 100 / Turnover"
+msgstr ""
+
+#. module: product_margin
+#: view:product.margin:0
+msgid "Open Margins"
+msgstr ""
+
+#. module: product_margin
+#: selection:product.margin,invoice_state:0
+#: selection:product.product,invoice_state:0
+msgid "Open and Paid"
+msgstr "የተከፈተና የትከፈል"
+
+#. module: product_margin
+#: view:product.product:0
+msgid "Sales"
+msgstr "ሽያጭ"
+
+#. module: product_margin
+#: model:ir.model,name:product_margin.model_product_product
+msgid "Product"
+msgstr "ውጤት"
+
+#. module: product_margin
+#: view:product.margin:0
+msgid "General Information"
+msgstr "አጠቃላይ መርጃ"
+
+#. module: product_margin
+#: field:product.product,purchase_gap:0
+msgid "Purchase Gap"
+msgstr "የግዢ ክፍተት"
+
+#. module: product_margin
+#: view:product.margin:0
+msgid "Cancel"
+msgstr "መሰረዝ"
+
+#. module: product_margin
+#: view:product.product:0
+msgid "Margins"
+msgstr "በአንድ መጠን ሲጨምር"
+
+#. module: product_margin
+#: help:product.product,sale_num_invoiced:0
+msgid "Sum of Quantity in Customer Invoices"
+msgstr ""
+
+#. module: product_margin
+#: view:product.margin:0
+msgid "or"
+msgstr "ወይም"
+
+#. module: product_margin
+#: model:ir.model,name:product_margin.model_product_margin
+msgid "Product Margin"
+msgstr "የእቃው መጨመር"
diff --git a/addons/project/project.py b/addons/project/project.py
index e5dcf5de2cf..f6bd55c2fb6 100644
--- a/addons/project/project.py
+++ b/addons/project/project.py
@@ -745,7 +745,7 @@ class task(osv.osv):
_columns = {
'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
- 'name': fields.char('Task Summary', size=128, required=True, select=True),
+ 'name': fields.char('Task Summary', track_visibility='onchange', size=128, required=True, select=True),
'description': fields.text('Description'),
'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index b47320b1e79..ed131670737 100644
--- a/addons/purchase/purchase.py
+++ b/addons/purchase/purchase.py
@@ -446,12 +446,7 @@ class purchase_order(osv.osv):
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time'
self.signal_send_rfq(cr, uid, ids)
- datas = {
- 'model': 'purchase.order',
- 'ids': ids,
- 'form': self.read(cr, uid, ids[0], context=context),
- }
- return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True}
+ return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
#TODO: implement messages system
def wkf_confirm_order(self, cr, uid, ids, context=None):
@@ -634,10 +629,9 @@ class purchase_order(osv.osv):
'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
'date': self.date_to_datetime(cr, uid, order.date_order, context),
- 'partner_id': order.dest_address_id.id or order.partner_id.id,
+ 'partner_id': order.partner_id.id,
'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
'type': 'in',
- 'partner_id': order.dest_address_id.id or order.partner_id.id,
'purchase_id': order.id,
'company_id': order.company_id.id,
'move_lines' : [],
diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py
index 0dae5f41288..5cff7b4aae2 100644
--- a/addons/report/controllers/main.py
+++ b/addons/report/controllers/main.py
@@ -19,459 +19,62 @@
#
##############################################################################
-from openerp.osv.osv import except_osv
-from openerp.addons.web import http
-from openerp.tools.translate import _
-from openerp.addons.web.http import request
-import openerp.tools.config as config
+from openerp.addons.web.http import Controller, route, request
-import time
-import base64
-import logging
-import tempfile
-import lxml.html
-import subprocess
import simplejson
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-import psutil
-import signal
-import os
-from distutils.version import LooseVersion
-
-
+import urlparse
from werkzeug import exceptions
-from werkzeug.test import Client
-from werkzeug.wrappers import BaseResponse
-from werkzeug.datastructures import Headers
from reportlab.graphics.barcode import createBarcodeDrawing
-_logger = logging.getLogger(__name__)
-try:
- from pyPdf import PdfFileWriter, PdfFileReader
-except ImportError:
- PdfFileWriter = PdfFileReader = None
+class ReportController(Controller):
-class Report(http.Controller):
-
- @http.route(['/report//'], type='http', auth='user', website=True, multilang=True)
- def report_html(self, reportname, docids, **kwargs):
- """This is the generic route for QWeb reports. It is used for reports
- which do not need to preprocess the data (i.e. reports that just display
- fields of a record).
-
- It is given a ~fully qualified report name, for instance 'account.report_invoice'.
- Based on it, we know the module concerned and the name of the template. With the
- name of the template, we will make a search on the ir.actions.reports.xml table and
- get the record associated to finally know the model this template refers to.
-
- There is a way to declare the report (in module_report(s).xml) that you must respect:
- id="action_report_model"
- model="module.model" # To know which model the report refers to
- string="Invoices"
- report_type="qweb-pdf" # or qweb-html
- name="module.template_name"
- file="module.template_name"
-
- If you don't want your report listed under the print button, just add
- 'menu=False'.
- """
- ids = [int(i) for i in docids.split(',')]
- ids = list(set(ids))
- report = self._get_report_from_name(reportname)
- report_obj = request.registry[report.model]
- docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
-
- docargs = {
- 'doc_ids': ids,
- 'doc_model': report.model,
- 'docs': docs,
- }
-
- return request.registry['report'].render(request.cr, request.uid, [], report.report_name,
- docargs, context=request.context)
-
- @http.route(['/report/pdf/'], type='http', auth="user", website=True)
- def report_pdf(self, path=None, landscape=False, **post):
- """Route converting any reports to pdf. It will get the html-rendered report, extract
- header, page and footer in order to prepare minimal html pages that will be further passed
- to wkhtmltopdf.
-
- :param path: URL of the report (e.g. /report/account.report_invoice/1)
- :returns: a response with 'application/pdf' headers and the pdf as content
- """
+ #------------------------------------------------------
+ # Generic reports controller
+ #------------------------------------------------------
+ @route('/report//', type='http', auth='user', website=True, multilang=True)
+ def report_html(self, reportname, docids):
cr, uid, context = request.cr, request.uid, request.context
+ docids = self._eval_params(docids)
+ return request.registry['report'].get_html(cr, uid, docids, reportname, context=context)
- # Get the report we are working on.
- # Pattern is /report/module.reportname(?a=1)
- reportname_in_path = path.split('/')[1].split('?')[0]
- report = self._get_report_from_name(reportname_in_path)
-
- # Check attachment_use field. If set to true and an existing pdf is already saved, load
- # this one now. If not, mark save it.
- save_in_attachment = {}
-
- if report.attachment_use is True:
- # Get the record ids we are working on.
- path_ids = [int(i) for i in path.split('/')[2].split('?')[0].split(',')]
-
- save_in_attachment['model'] = report.model
- save_in_attachment['loaded_documents'] = {}
-
- for path_id in path_ids:
- obj = request.registry[report.model].browse(cr, uid, path_id)
- filename = eval(report.attachment, {'object': obj, 'time': time})
-
- if filename is False: # May be false if, for instance, the record is in draft state
- continue
- else:
- alreadyindb = [('datas_fname', '=', filename),
- ('res_model', '=', report.model),
- ('res_id', '=', path_id)]
-
- attach_ids = request.registry['ir.attachment'].search(cr, uid, alreadyindb)
- if attach_ids:
- # Add the loaded pdf in the loaded_documents list
- pdf = request.registry['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
- pdf = base64.decodestring(pdf)
- save_in_attachment['loaded_documents'][path_id] = pdf
- _logger.info('The PDF document %s was loaded from the database' % filename)
- else:
- # Mark current document to be saved
- save_in_attachment[path_id] = filename
-
- # Get the paperformat associated to the report. If there is not, get the one associated to
- # the company.
- if not report.paperformat_id:
- user = request.registry['res.users'].browse(cr, uid, uid, context=context)
- paperformat = user.company_id.paperformat_id
- else:
- paperformat = report.paperformat_id
-
- # Get the html report.
- html = self._get_url_content('/' + path, post)[0]
- subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
- css = '' # Local css
-
- headerhtml = []
- contenthtml = []
- footerhtml = []
- base_url = request.registry['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
-
- minimalhtml = """
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {2}
-
-"""
-
- # The retrieved html report must be simplified. We convert it into a xml tree
- # via lxml in order to extract headers, footers and content.
- try:
- root = lxml.html.fromstring(html)
-
- for node in root.xpath("//html/head/style"):
- css += node.text
-
- for node in root.xpath("//div[@class='header']"):
- body = lxml.html.tostring(node)
- header = minimalhtml.format(css, subst, body, base_url)
- headerhtml.append(header)
-
- for node in root.xpath("//div[@class='footer']"):
- body = lxml.html.tostring(node)
- footer = minimalhtml.format(css, subst, body, base_url)
- footerhtml.append(footer)
-
- for node in root.xpath("//div[@class='page']"):
- # Previously, we marked some reports to be saved in attachment via their ids, so we
- # must set a relation between report ids and report's content. We use the QWeb
- # branding in order to do so: searching after a node having a data-oe-model
- # attribute with the value of the current report model and read its oe-id attribute
- oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']")
- if oemodelnode is not None:
- reportid = oemodelnode.get('data-oe-id', False)
- if reportid is not False:
- reportid = int(reportid)
- else:
- reportid = False
-
- body = lxml.html.tostring(node)
- reportcontent = minimalhtml.format(css, '', body, base_url)
- contenthtml.append(tuple([reportid, reportcontent]))
-
- except lxml.etree.XMLSyntaxError:
- contenthtml = []
- contenthtml.append(html)
- save_in_attachment = {} # Don't save this potentially malformed document
-
- # Get paperformat arguments set in the root html tag. They are prioritized over
- # paperformat-record arguments.
- specific_paperformat_args = {}
- for attribute in root.items():
- if attribute[0].startswith('data-report-'):
- specific_paperformat_args[attribute[0]] = attribute[1]
-
- # Execute wkhtmltopdf process.
- pdf = self._generate_wkhtml_pdf(headerhtml, footerhtml, contenthtml, landscape,
- paperformat, specific_paperformat_args, save_in_attachment)
-
- return self._make_pdf_response(pdf)
-
- def _get_url_content(self, url, post=None):
- """Resolve an internal webpage url and return its content with the help of
- werkzeug.test.client.
-
- :param url: string representing the url to resolve
- :param post: a dict representing the query string
- :returns: a tuple str(html), int(statuscode)
- """
- # Rebuilding the query string.
- if post:
- url += '?'
- url += '&'.join('%s=%s' % (k, v) for (k, v) in post.iteritems())
-
- # We have to pass the current headers in order to see the report.
- reqheaders = Headers(request.httprequest.headers)
- response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
- follow_redirects=True)
- content = response.data
-
- try:
- content = content.decode('utf-8')
- except UnicodeDecodeError:
- pass
-
- return tuple([content, response.headers])
-
- def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape,
- paperformat, spec_paperformat_args=None, save_in_attachment=None):
- """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
- document.
-
- :param header: list of string containing the headers
- :param footer: list of string containing the footers
- :param bodies: list of string containing the reports
- :param landscape: boolean to force the pdf to be rendered under a landscape format
- :param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
- :param specific_paperformat_args: dict of prioritized paperformat arguments
- :param save_in_attachment: dict of reports to save/load in/from the db
- :returns: Content of the pdf as a string
- """
- command = ['wkhtmltopdf']
- tmp_dir = tempfile.gettempdir()
-
- command_args = []
- # Passing the cookie in order to resolve URL.
- command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']])
-
- # Display arguments
- if paperformat:
- command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
-
- if landscape and '--orientation' in command_args:
- command_args_copy = list(command_args)
- for index, elem in enumerate(command_args_copy):
- if elem == '--orientation':
- del command_args[index]
- del command_args[index]
- command_args.extend(['--orientation', 'landscape'])
- elif landscape and not '--orientation' in command_args:
- command_args.extend(['--orientation', 'landscape'])
-
- pdfdocuments = []
- # HTML to PDF thanks to WKhtmltopdf
- for index, reporthtml in enumerate(bodies):
- command_arg_local = []
- pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
- mode='w+b')
- # Directly load the document if we have it
- if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
- pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
- pdfreport.seek(0)
- pdfdocuments.append(pdfreport)
- continue
-
- # Header stuff
- if headers:
- head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
- dir=tmp_dir, mode='w+')
- head_file.write(headers[index])
- head_file.seek(0)
- command_arg_local.extend(['--header-html', head_file.name])
-
- # Footer stuff
- if footers:
- foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
- dir=tmp_dir, mode='w+')
- foot_file.write(footers[index])
- foot_file.seek(0)
- command_arg_local.extend(['--footer-html', foot_file.name])
-
- # Body stuff
- content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
- dir=tmp_dir, mode='w+')
- content_file.write(reporthtml[1])
- content_file.seek(0)
-
- try:
- # If the server is running with only one worker, increase it to two to be able
- # to serve the http request from wkhtmltopdf.
- if config['workers'] == 1:
- ppid = psutil.Process(os.getpid()).ppid
- os.kill(ppid, signal.SIGTTIN)
-
- wkhtmltopdf = command + command_args + command_arg_local
- wkhtmltopdf += [content_file.name] + [pdfreport.name]
-
- process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- out, err = process.communicate()
-
- if config['workers'] == 1:
- os.kill(ppid, signal.SIGTTOU)
-
- if process.returncode != 0:
- raise except_osv(_('Report (PDF)'),
- _('wkhtmltopdf failed with error code = %s. '
- 'Message: %s') % (str(process.returncode), err))
-
- # Save the pdf in attachment if marked
- if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
- attachment = {
- 'name': save_in_attachment.get(reporthtml[0]),
- 'datas': base64.encodestring(pdfreport.read()),
- 'datas_fname': save_in_attachment.get(reporthtml[0]),
- 'res_model': save_in_attachment.get('model'),
- 'res_id': reporthtml[0],
- }
- request.registry['ir.attachment'].create(request.cr, request.uid, attachment)
- _logger.info('The PDF document %s is now saved in the '
- 'database' % attachment['name'])
-
- pdfreport.seek(0)
- pdfdocuments.append(pdfreport)
-
- if headers:
- head_file.close()
- if footers:
- foot_file.close()
- except:
- raise
-
- # Get and return the full pdf
- if len(pdfdocuments) == 1:
- content = pdfdocuments[0].read()
- pdfdocuments[0].close()
- else:
- content = self._merge_pdf(pdfdocuments)
-
- return content
-
- def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
- """Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat
- record.
-
- :paperformat: ir.actions.report.paperformat record associated to a document
- :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
- :returns: list of string containing the wkhtmltopdf arguments
- """
- command_args = []
- if paperformat.format and paperformat.format != 'custom':
- command_args.extend(['--page-size', paperformat.format])
-
- if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
- command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
- command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
-
- if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
- command_args.extend(['--margin-top',
- str(specific_paperformat_args['data-report-margin-top'])])
- elif paperformat.margin_top:
- command_args.extend(['--margin-top', str(paperformat.margin_top)])
-
- if paperformat.margin_left:
- command_args.extend(['--margin-left', str(paperformat.margin_left)])
- if paperformat.margin_bottom:
- command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
- if paperformat.margin_right:
- command_args.extend(['--margin-right', str(paperformat.margin_right)])
- if paperformat.orientation:
- command_args.extend(['--orientation', str(paperformat.orientation)])
- if paperformat.header_spacing:
- command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
- if paperformat.header_line:
- command_args.extend(['--header-line'])
- if paperformat.dpi:
- command_args.extend(['--dpi', str(paperformat.dpi)])
-
- return command_args
-
- def _get_report_from_name(self, report_name):
- """Get the first record of ir.actions.report.xml having the argument as value for
- the field report_name.
- """
- report_obj = request.registry['ir.actions.report.xml']
- qwebtypes = ['qweb-pdf', 'qweb-html']
-
- idreport = report_obj.search(request.cr, request.uid,
- [('report_type', 'in', qwebtypes),
- ('report_name', '=', report_name)])
-
- report = report_obj.browse(request.cr, request.uid, idreport[0],
- context=request.context)
- return report
-
- def _make_pdf_response(self, pdf):
- """Make a request response for a PDF file with correct http headers.
-
- :param pdf: content of a pdf in a string
- :returns: request response for a pdf document
- """
- pdfhttpheaders = [('Content-Type', 'application/pdf'),
- ('Content-Length', len(pdf))]
+ @route('/report/pdf/report//', type='http', auth="user", website=True)
+ def report_pdf(self, reportname, docids):
+ cr, uid, context = request.cr, request.uid, request.context
+ docids = self._eval_params(docids)
+ pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context)
+ pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
- def _merge_pdf(self, documents):
- """Merge PDF files into one.
+ #------------------------------------------------------
+ # Particular reports controller
+ #------------------------------------------------------
+ @route('/report/', type='http', auth='user', website=True, multilang=True)
+ def report_html_particular(self, reportname, **data):
+ cr, uid, context = request.cr, request.uid, request.context
+ report_obj = request.registry['report']
+ data = self._eval_params(data) # Sanitizing
+ return report_obj.get_html(cr, uid, [], reportname, data=data, context=context)
- :param documents: list of pdf files
- :returns: string containing the merged pdf
- """
- writer = PdfFileWriter()
- for document in documents:
- reader = PdfFileReader(file(document.name, "rb"))
- for page in range(0, reader.getNumPages()):
- writer.addPage(reader.getPage(page))
- document.close()
- merged = StringIO.StringIO()
- writer.write(merged)
- merged.seek(0)
- content = merged.read()
- merged.close()
- return content
+ @route('/report/pdf/report/', type='http', auth='user', website=True, multilang=True)
+ def report_pdf_particular(self, reportname, **data):
+ cr, uid, context = request.cr, request.uid, request.context
+ report_obj = request.registry['report']
+ data = self._eval_params(data) # Sanitizing
+ pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context)
+ pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
+ return request.make_response(pdf, headers=pdfhttpheaders)
- @http.route(['/report/barcode', '/report/barcode//'], type='http', auth="user")
- def barcode(self, type, value, width=300, height=50):
+ #------------------------------------------------------
+ # Misc. route utils
+ #------------------------------------------------------
+ @route(['/report/barcode', '/report/barcode//'], type='http', auth="user")
+ def report_barcode(self, type, value, width=300, height=50):
"""Contoller able to render barcode images thanks to reportlab.
Samples:
-
+
:param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39',
'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93',
@@ -488,52 +91,69 @@ class Report(http.Controller):
return request.make_response(barcode, headers=[('Content-Type', 'image/png')])
- @http.route('/report/download', type='http', auth="user")
- def report_attachment(self, data, token):
+ @route(['/report/download'], type='http', auth="user", website=True)
+ def report_download(self, data, token):
"""This function is used by 'qwebactionmanager.js' in order to trigger the download of
- a report of any type.
+ a pdf report.
- :param data: a javasscript array JSON.stringified containg report internal url ([0]) and
+ :param data: a javascript array JSON.stringified containg report internal url ([0]) and
type [1]
:returns: Response with a filetoken cookie and an attachment header
"""
requestcontent = simplejson.loads(data)
url, type = requestcontent[0], requestcontent[1]
- file, fileheaders = self._get_url_content(url)
-
if type == 'qweb-pdf':
- response = self._make_pdf_response(file)
- response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;')
- elif type == 'controller':
- response = request.make_response(file)
- response.headers.add('Content-Disposition', fileheaders['Content-Disposition'])
- response.headers.add('Content-Type', fileheaders['Content-Type'])
+ reportname = url.split('/report/pdf/report/')[1].split('?')[0].split('/')[0]
+
+ if '?' not in url:
+ # Generic report:
+ docids = url.split('/')[-1]
+ response = self.report_pdf(reportname, docids)
+ else:
+ # Particular report:
+ querystring = url.split('?')[1]
+ querystring = dict(urlparse.parse_qsl(querystring))
+ response = self.report_pdf_particular(reportname, **querystring)
+
+ response.headers.add('Content-Disposition', 'attachment; filename=%s.pdf;' % reportname)
+ response.set_cookie('fileToken', token)
+ return response
+ elif type =='controller':
+ from werkzeug.test import Client
+ from werkzeug.wrappers import BaseResponse
+ from werkzeug.datastructures import Headers
+ reqheaders = Headers(request.httprequest.headers)
+ response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, follow_redirects=True)
+ response.set_cookie('fileToken', token)
+ return response
else:
return
- response.headers.add('Content-Length', len(file))
- response.set_cookie('fileToken', token)
- return response
-
- @http.route('/report/check_wkhtmltopdf', type='json', auth="user")
+ @route(['/report/check_wkhtmltopdf'], type='json', auth="user")
def check_wkhtmltopdf(self):
- """Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf
- cannot be found, return False.
+ return request.registry['report']._check_wkhtmltopdf()
+
+ def _eval_params(self, param):
+ """Parse a dict generated by the webclient (javascript) into a python dict.
"""
- try:
- process = subprocess.Popen(['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- out, err = process.communicate()
- if err:
- raise
-
- version = out.splitlines()[1].strip()
- version = version.split(' ')[1]
-
- if LooseVersion(version) < LooseVersion('0.12.0'):
- _logger.warning('Upgrade WKHTMLTOPDF to (at least) 0.12.0')
- return 'upgrade'
-
- return True
- except:
- _logger.error('You need WKHTMLTOPDF to print a pdf version of this report.')
- return False
+ if isinstance(param, dict):
+ for key, value in param.iteritems():
+ if value.lower() == 'false':
+ param[key] = False
+ elif value.lower() == 'true':
+ param[key] = True
+ elif ',' in value:
+ param[key] = [int(i) for i in value.split(',')]
+ else:
+ try:
+ param[key] = int(value)
+ except (ValueError, TypeError):
+ pass
+ else:
+ if isinstance(param, (str, unicode)):
+ param = [int(i) for i in param.split(',')]
+ if isinstance(param, list):
+ param = list(set(param))
+ if isinstance(param, int):
+ param = [param]
+ return param
diff --git a/addons/report/models/report.py b/addons/report/models/report.py
index 8a2bc21b3b0..8abe8e544eb 100644
--- a/addons/report/models/report.py
+++ b/addons/report/models/report.py
@@ -19,54 +19,85 @@
#
##############################################################################
-from openerp.addons.web.http import request
from openerp.osv import osv
-from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
-from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, config
+from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
+import os
import time
+import psutil
+import signal
+import base64
+import logging
+import tempfile
+import lxml.html
+import cStringIO
+import subprocess
from datetime import datetime
-
-from werkzeug.datastructures import Headers
-from werkzeug.wrappers import BaseResponse
-from werkzeug.test import Client
+from functools import partial
+from distutils.version import LooseVersion
+try:
+ from pyPdf import PdfFileWriter, PdfFileReader
+except ImportError:
+ PdfFileWriter = PdfFileReader = None
-def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
- return len((datetime.now()).strftime(date_format))
+_logger = logging.getLogger(__name__)
-class report(osv.Model):
+"""Check the presence of wkhtmltopdf and return its version."""
+wkhtmltopdf_state = 'install'
+try:
+ process = subprocess.Popen(
+ ['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+except OSError:
+ _logger.error('You need wkhtmltopdf to print a pdf version of the reports.')
+else:
+ out, err = process.communicate()
+ version = out.splitlines()[1].strip()
+ version = version.split(' ')[1]
+ if LooseVersion(version) < LooseVersion('0.12.0'):
+ _logger.warning('Upgrade wkhtmltopdf to (at least) 0.12.0')
+ wkhtmltopdf_state = 'upgrade'
+ wkhtmltopdf_state = 'ok'
+
+
+class Report(osv.Model):
_name = "report"
_description = "Report"
public_user = None
- def get_digits(self, obj=None, f=None, dp=None):
+ #--------------------------------------------------------------------------
+ # Extension of ir_ui_view.render with arguments frequently used in reports
+ #--------------------------------------------------------------------------
+
+ def _get_digits(self, cr, uid, obj=None, f=None, dp=None):
d = DEFAULT_DIGITS = 2
if dp:
decimal_precision_obj = self.pool['decimal.precision']
- ids = decimal_precision_obj.search(request.cr, request.uid, [('name', '=', dp)])
+ ids = decimal_precision_obj.search(cr, uid, [('name', '=', dp)])
if ids:
- d = decimal_precision_obj.browse(request.cr, request.uid, ids)[0].digits
+ d = decimal_precision_obj.browse(cr, uid, ids)[0].digits
elif obj and f:
res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
if isinstance(res_digits, tuple):
d = res_digits[1]
else:
- d = res_digits(request.cr)[1]
+ d = res_digits(cr)[1]
elif (hasattr(obj, '_field') and
isinstance(obj._field, (float_field, function_field)) and
obj._field.digits):
d = obj._field.digits[1] or DEFAULT_DIGITS
return d
- def _get_lang_dict(self):
+ def _get_lang_dict(self, cr, uid):
pool_lang = self.pool['res.lang']
lang = self.localcontext.get('lang', 'en_US') or 'en_US'
- lang_ids = pool_lang.search(request.cr, request.uid, [('code', '=', lang)])[0]
- lang_obj = pool_lang.browse(request.cr, request.uid, lang_ids)
+ lang_ids = pool_lang.search(cr, uid, [('code', '=', lang)])[0]
+ lang_obj = pool_lang.browse(cr, uid, lang_ids)
lang_dict = {
'lang_obj': lang_obj,
'date_format': lang_obj.date_format,
@@ -76,7 +107,7 @@ class report(osv.Model):
self.default_lang[lang] = self.lang_dict.copy()
return True
- def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
+ def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False, cr=None, uid=None):
"""
Assuming 'Account' decimal.precision=3:
formatLang(value) -> digits=2 (default)
@@ -84,17 +115,20 @@ class report(osv.Model):
formatLang(value, dp='Account') -> digits=3
formatLang(value, digits=5, dp='Account') -> digits=5
"""
+ def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
+ return len((datetime.now()).strftime(date_format))
+
if digits is None:
if dp:
- digits = self.get_digits(dp=dp)
+ digits = self._get_digits(cr, uid, dp=dp)
else:
- digits = self.get_digits(value)
+ digits = self._get_digits(cr, uid, value)
if isinstance(value, (str, unicode)) and not value:
return ''
if not self.lang_dict_called:
- self._get_lang_dict()
+ self._get_lang_dict(cr, uid)
self.lang_dict_called = True
if date or date_time:
@@ -117,9 +151,7 @@ class report(osv.Model):
date = datetime(*value.timetuple()[:6])
if date_time:
# Convert datetime values to the expected client/context timezone
- date = datetime_field.context_timestamp(request.cr, request.uid,
- timestamp=date,
- context=self.localcontext)
+ date = datetime_field.context_timestamp(cr, uid, timestamp=date, context=self.localcontext)
return date.strftime(date_format.encode('utf-8'))
res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
@@ -150,7 +182,7 @@ class report(osv.Model):
'tz': context.get('tz'),
'uid': context.get('uid'),
}
- self._get_lang_dict()
+ self._get_lang_dict(cr, uid)
view_obj = self.pool['ir.ui.view']
@@ -177,51 +209,180 @@ class report(osv.Model):
qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx)
return view_obj.render(cr, uid, template, qcontext, context=ctx)
- current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
-
- # Website independance code
- website = False
- res_company = current_user.company_id
-
- try:
- website = request.website
- res_company = request.website.company_id
- except:
- pass
-
values.update({
'time': time,
- 'user': current_user,
- 'user_id': current_user.id,
- 'formatLang': self.formatLang,
- 'get_digits': self.get_digits,
+ 'formatLang': partial(self.formatLang, cr=cr, uid=uid),
+ 'get_digits': self._get_digits,
'render_doc': render_doc,
- 'website': website,
- 'res_company': res_company,
+ 'editable': True, # Will active inherit_branding
+ 'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id,
+ 'website': False, # Will be overidden by ir.ui.view if the request has website enabled
})
return view_obj.render(cr, uid, template, values, context=context)
- def get_pdf(self, report, record_id, context=None):
- """Used to return the content of a generated PDF.
+ #--------------------------------------------------------------------------
+ # Main reports methods
+ #--------------------------------------------------------------------------
- :returns: pdf
+ def get_html(self, cr, uid, ids, report_name, data=None, context=None):
+ """This method generates and returns html version of a report.
"""
- url = '/report/pdf/report/' + report.report_file + '/' + str(record_id)
- reqheaders = Headers(request.httprequest.headers)
- reqheaders.pop('Accept')
- reqheaders.add('Accept', 'application/pdf')
- reqheaders.pop('Content-Type')
- reqheaders.add('Content-Type', 'text/plain')
- response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
- follow_redirects=True)
- return response.data
+ # If the report is using a custom model to render its html, we must use it.
+ # Otherwise, fallback on the generic html rendering.
+ try:
+ report_model_name = 'report.%s' % report_name
+ particularreport_obj = self.pool[report_model_name]
+ return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context)
+ except KeyError:
+ report = self._get_report_from_name(cr, uid, report_name)
+ report_obj = self.pool[report.model]
+ docs = report_obj.browse(cr, uid, ids, context=context)
+ docargs = {
+ 'doc_ids': ids,
+ 'doc_model': report.model,
+ 'docs': docs,
+ }
+ return self.render(cr, uid, [], report.report_name, docargs, context=context)
+
+ def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None):
+ """This method generates and returns pdf version of a report.
+ """
+ if context is None:
+ context = {}
+
+ if html is None:
+ html = self.get_html(cr, uid, ids, report_name, data=data, context=context)
+
+ html = html.decode('utf-8')
+
+ # Get the ir.actions.report.xml record we are working on.
+ report = self._get_report_from_name(cr, uid, report_name)
+
+ # Check attachment_use field. If set to true and an existing pdf is already saved, load
+ # this one now. Else, mark save it.
+ save_in_attachment = {}
+
+ if report.attachment_use is True:
+ save_in_attachment['model'] = report.model
+ save_in_attachment['loaded_documents'] = {}
+
+ for record_id in ids:
+ obj = self.pool[report.model].browse(cr, uid, record_id)
+ filename = eval(report.attachment, {'object': obj, 'time': time})
+
+ if filename is False: # May be false if, for instance, the record is in draft state
+ continue
+ else:
+ alreadyindb = [('datas_fname', '=', filename),
+ ('res_model', '=', report.model),
+ ('res_id', '=', record_id)]
+
+ attach_ids = self.pool['ir.attachment'].search(cr, uid, alreadyindb)
+ if attach_ids:
+ # Add the loaded pdf in the loaded_documents list
+ pdf = self.pool['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
+ pdf = base64.decodestring(pdf)
+ save_in_attachment['loaded_documents'][record_id] = pdf
+ _logger.info('The PDF document %s was loaded from the database' % filename)
+ else:
+ # Mark current document to be saved
+ save_in_attachment[id] = filename
+
+ # Get the paperformat associated to the report, otherwise fallback on the company one.
+ if not report.paperformat_id:
+ user = self.pool['res.users'].browse(cr, uid, uid)
+ paperformat = user.company_id.paperformat_id
+ else:
+ paperformat = report.paperformat_id
+
+ # Preparing the minimal html pages
+ #subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
+ subst = " "
+ css = '' # Will contain local css
+
+ headerhtml = []
+ contenthtml = []
+ footerhtml = []
+ base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
+
+ minimalhtml = """
+
+
+
+
+
+
+
+
+
+ {subst}
+
+
+ {body}
+
+"""
+
+ # The retrieved html report must be simplified. We convert it into a xml tree
+ # via lxml in order to extract headers, footers and content.
+ try:
+ root = lxml.html.fromstring(html)
+
+ for node in root.xpath("//html/head/style"):
+ css += node.text
+
+ for node in root.xpath("//div[@class='header']"):
+ body = lxml.html.tostring(node)
+ header = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
+ headerhtml.append(header)
+
+ for node in root.xpath("//div[@class='footer']"):
+ body = lxml.html.tostring(node)
+ footer = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
+ footerhtml.append(footer)
+
+ for node in root.xpath("//div[@class='page']"):
+ # Previously, we marked some reports to be saved in attachment via their ids, so we
+ # must set a relation between report ids and report's content. We use the QWeb
+ # branding in order to do so: searching after a node having a data-oe-model
+ # attribute with the value of the current report model and read its oe-id attribute
+ oemodelnode = node.find(".//*[@data-oe-model='%s']" % report.model)
+ if oemodelnode is not None:
+ reportid = oemodelnode.get('data-oe-id')
+ if reportid:
+ reportid = int(reportid)
+ else:
+ reportid = False
+
+ body = lxml.html.tostring(node)
+ reportcontent = minimalhtml.format(css=css, subst='', body=body, base_url=base_url)
+ contenthtml.append(tuple([reportid, reportcontent]))
+
+ except lxml.etree.XMLSyntaxError:
+ contenthtml = []
+ contenthtml.append(html)
+ save_in_attachment = {} # Don't save this potentially malformed document
+
+ # Get paperformat arguments set in the root html tag. They are prioritized over
+ # paperformat-record arguments.
+ specific_paperformat_args = {}
+ for attribute in root.items():
+ if attribute[0].startswith('data-report-'):
+ specific_paperformat_args[attribute[0]] = attribute[1]
+
+ # Run wkhtmltopdf process
+ pdf = self._generate_wkhtml_pdf(
+ cr, uid, headerhtml, footerhtml, contenthtml, context.get('landscape'),
+ paperformat, specific_paperformat_args, save_in_attachment
+ )
+ return pdf
def get_action(self, cr, uid, ids, report_name, datas=None, context=None):
- """Used to return an action of type ir.actions.report.xml.
+ """Return an action of type ir.actions.report.xml.
:param report_name: Name of the template to generate an action for
"""
+ # TODO: return the action for the ids passed in args
if context is None:
context = {}
@@ -249,26 +410,203 @@ class report(osv.Model):
return action
- def eval_params(self, dict_param):
- """Parse a dictionary generated by the webclient (javascript) into a dictionary
- understandable by a wizard controller (python).
- """
- for key, value in dict_param.iteritems():
- if value.lower() == 'false':
- dict_param[key] = False
- elif value.lower() == 'true':
- dict_param[key] = True
- elif ',' in value:
- dict_param[key] = [int(i) for i in value.split(',')]
- elif '%2C' in value:
- dict_param[key] = [int(i) for i in value.split('%2C')]
- else:
- try:
- i = int(value)
- dict_param[key] = i
- except (ValueError, TypeError):
- pass
+ #--------------------------------------------------------------------------
+ # Report generation helpers
+ #--------------------------------------------------------------------------
- data = {}
- data['form'] = dict_param
- return data
+ def _check_wkhtmltopdf(self):
+ return wkhtmltopdf_state
+
+ def _generate_wkhtml_pdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None):
+ """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
+ document.
+
+ :param header: list of string containing the headers
+ :param footer: list of string containing the footers
+ :param bodies: list of string containing the reports
+ :param landscape: boolean to force the pdf to be rendered under a landscape format
+ :param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
+ :param specific_paperformat_args: dict of prioritized paperformat arguments
+ :param save_in_attachment: dict of reports to save/load in/from the db
+ :returns: Content of the pdf as a string
+ """
+ command = ['wkhtmltopdf']
+ command_args = []
+ tmp_dir = tempfile.gettempdir()
+
+ # Passing the cookie to wkhtmltopdf in order to resolve URL.
+ try:
+ from openerp.addons.web.http import request
+ command_args.extend(['--cookie', 'session_id', request.session.sid])
+ except AttributeError:
+ pass
+
+ # Display arguments
+ if paperformat:
+ command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
+
+ if landscape and '--orientation' in command_args:
+ command_args_copy = list(command_args)
+ for index, elem in enumerate(command_args_copy):
+ if elem == '--orientation':
+ del command_args[index]
+ del command_args[index]
+ command_args.extend(['--orientation', 'landscape'])
+ elif landscape and not '--orientation' in command_args:
+ command_args.extend(['--orientation', 'landscape'])
+
+ pdfdocuments = []
+ # HTML to PDF thanks to WKhtmltopdf
+ for index, reporthtml in enumerate(bodies):
+ command_arg_local = []
+ pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
+ mode='w+b')
+ # Directly load the document if we have it
+ if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
+ pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
+ pdfreport.flush()
+ pdfdocuments.append(pdfreport)
+ continue
+
+ # Header stuff
+ if headers:
+ head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
+ dir=tmp_dir, mode='w+')
+ head_file.write(headers[index])
+ head_file.flush()
+ command_arg_local.extend(['--header-html', head_file.name])
+
+ # Footer stuff
+ if footers:
+ foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
+ dir=tmp_dir, mode='w+')
+ foot_file.write(footers[index])
+ foot_file.flush()
+ command_arg_local.extend(['--footer-html', foot_file.name])
+
+ # Body stuff
+ content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
+ dir=tmp_dir, mode='w+')
+ content_file.write(reporthtml[1])
+ content_file.flush()
+
+ try:
+ # If the server is running with only one worker, ask to create a secund to be able
+ # to serve the http request of wkhtmltopdf subprocess.
+ if config['workers'] == 1:
+ ppid = psutil.Process(os.getpid()).ppid
+ os.kill(ppid, signal.SIGTTIN)
+
+ wkhtmltopdf = command + command_args + command_arg_local
+ wkhtmltopdf += [content_file.name] + [pdfreport.name]
+
+ process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = process.communicate()
+
+ if config['workers'] == 1:
+ os.kill(ppid, signal.SIGTTOU)
+
+ if process.returncode != 0:
+ raise osv.except_osv(_('Report (PDF)'),
+ _('wkhtmltopdf failed with error code = %s. '
+ 'Message: %s') % (str(process.returncode), err))
+
+ # Save the pdf in attachment if marked
+ if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
+ attachment = {
+ 'name': save_in_attachment.get(reporthtml[0]),
+ 'datas': base64.encodestring(pdfreport.read()),
+ 'datas_fname': save_in_attachment.get(reporthtml[0]),
+ 'res_model': save_in_attachment.get('model'),
+ 'res_id': reporthtml[0],
+ }
+ self.pool['ir.attachment'].create(cr, uid, attachment)
+ _logger.info('The PDF document %s is now saved in the '
+ 'database' % attachment['name'])
+
+ pdfreport.flush()
+ pdfdocuments.append(pdfreport)
+
+ if headers:
+ head_file.close()
+ if footers:
+ foot_file.close()
+ except:
+ raise
+
+ # Get and return the full pdf
+ if len(pdfdocuments) == 1:
+ content = pdfdocuments[0].read()
+ pdfdocuments[0].close()
+ else:
+ content = self._merge_pdf(pdfdocuments)
+
+ return content
+
+ def _get_report_from_name(self, cr, uid, report_name):
+ """Get the first record of ir.actions.report.xml having the ``report_name`` as value for
+ the field report_name.
+ """
+ report_obj = self.pool['ir.actions.report.xml']
+ qwebtypes = ['qweb-pdf', 'qweb-html']
+ conditions = [('report_type', 'in', qwebtypes), ('report_name', '=', report_name)]
+ idreport = report_obj.search(cr, uid, conditions)[0]
+ return report_obj.browse(cr, uid, idreport)
+
+ def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
+ """Build arguments understandable by wkhtmltopdf from a report.paperformat record.
+
+ :paperformat: report.paperformat record
+ :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
+ :returns: list of string representing the wkhtmltopdf arguments
+ """
+ command_args = []
+ if paperformat.format and paperformat.format != 'custom':
+ command_args.extend(['--page-size', paperformat.format])
+
+ if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
+ command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
+ command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
+
+ if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
+ command_args.extend(['--margin-top',
+ str(specific_paperformat_args['data-report-margin-top'])])
+ elif paperformat.margin_top:
+ command_args.extend(['--margin-top', str(paperformat.margin_top)])
+
+ if paperformat.margin_left:
+ command_args.extend(['--margin-left', str(paperformat.margin_left)])
+ if paperformat.margin_bottom:
+ command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
+ if paperformat.margin_right:
+ command_args.extend(['--margin-right', str(paperformat.margin_right)])
+ if paperformat.orientation:
+ command_args.extend(['--orientation', str(paperformat.orientation)])
+ if paperformat.header_spacing:
+ command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
+ if paperformat.header_line:
+ command_args.extend(['--header-line'])
+ if paperformat.dpi:
+ command_args.extend(['--dpi', str(paperformat.dpi)])
+
+ return command_args
+
+ def _merge_pdf(self, documents):
+ """Merge PDF files into one.
+
+ :param documents: list of pdf files
+ :returns: string containing the merged pdf
+ """
+ writer = PdfFileWriter()
+ for document in documents:
+ reader = PdfFileReader(file(document.name, "rb"))
+ for page in range(0, reader.getNumPages()):
+ writer.addPage(reader.getPage(page))
+ document.close()
+ merged = cStringIO.StringIO()
+ writer.write(merged)
+ merged.seek(0)
+ content = merged.read()
+ merged.close()
+ return content
diff --git a/addons/report/static/src/js/qwebactionmanager.js b/addons/report/static/src/js/qwebactionmanager.js
index 8d1550a6e2c..b75003942d2 100644
--- a/addons/report/static/src/js/qwebactionmanager.js
+++ b/addons/report/static/src/js/qwebactionmanager.js
@@ -1,4 +1,5 @@
openerp.report = function(instance) {
+ var wkhtmltopdf_state;
instance.web.ActionManager = instance.web.ActionManager.extend({
ir_actions_report_xml: function(action, options) {
@@ -11,8 +12,7 @@ openerp.report = function(instance) {
// QWeb reports
if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) {
-
- var report_url = ''
+ var report_url = '';
switch (action.report_type) {
case 'qweb-html':
report_url = '/report/' + action.report_name;
@@ -49,41 +49,49 @@ openerp.report = function(instance) {
}
if (action.report_type == 'qweb-html') {
// Open the html report in a popup
- window.open(report_url, '_blank', 'height=768,width=1024');
+ window.open(report_url, '_blank', 'height=900,width=1280');
instance.web.unblockUI();
return;
} else {
- // Trigger the download of the pdf/custom controller report
+ // Trigger the download of the pdf/controller report
var c = openerp.webclient.crashmanager;
- var response = new Array()
- response[0] = report_url
- response[1] = action.report_type
+ var response = new Array();
+ response[0] = report_url;
+ response[1] = action.report_type;
- openerp.session.rpc('/report/check_wkhtmltopdf').then(function (presence) {
- // Fallback of qweb-pdf if wkhtmltopdf is not installed
- if (!presence && action.report_type == 'qweb-pdf') {
- self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \
-system. The report will be shown in html.
\
-wkhtmltopdf.org'), true);
- window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
- instance.web.unblockUI();
- }
- else {
- if (presence == 'upgrade') {
- self.do_notify(_t('Report'), _t('You should upgrade your version of\
- Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\
- support for table-breaking between pages.
wkhtmltopdf.org'), true);
+ if (action.report_type == 'qweb-pdf') {
+ (wkhtmltopdf_state = wkhtmltopdf_state || openerp.session.rpc('/report/check_wkhtmltopdf')).then(function (presence) {
+ // Fallback of qweb-pdf if wkhtmltopdf is not installed
+ if (presence == 'install' && action.report_type == 'qweb-pdf') {
+ self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \
+ system. The report will be shown in html.
\
+ wkhtmltopdf.org'), true);
+ window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
+ instance.web.unblockUI();
+ } else {
+ if (presence == 'upgrade') {
+ self.do_notify(_t('Report'), _t('You should upgrade your version of\
+ Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\
+ support for table-breaking between pages.
wkhtmltopdf.org'), true);
+ }
+ self.session.get_file({
+ url: '/report/download',
+ data: {data: JSON.stringify(response)},
+ complete: openerp.web.unblockUI,
+ error: c.rpc_error.bind(c)
+ });
}
- self.session.get_file({
- url: '/report/download',
- data: {data: JSON.stringify(response)},
- complete: openerp.web.unblockUI,
- error: c.rpc_error.bind(c)
- });
- }
- });
- }
+ });
+ } else {
+ self.session.get_file({
+ url: '/report/download',
+ data: {data: JSON.stringify(response)},
+ complete: openerp.web.unblockUI,
+ error: c.rpc_error.bind(c)
+ });
+ }
+ }
} else {
return self._super(action, options);
}
diff --git a/addons/report/tests/test_reports.py b/addons/report/tests/test_reports.py
index 361e0a7aa0f..dda14442709 100644
--- a/addons/report/tests/test_reports.py
+++ b/addons/report/tests/test_reports.py
@@ -1,26 +1,36 @@
# -*- coding: utf-8 -*-
-import logging
+import logging
import openerp
-import urllib2
+
_logger = logging.getLogger(__name__)
-class TestReports(openerp.tests.HttpCase):
+
+@openerp.tests.common.at_install(False)
+@openerp.tests.common.post_install(True)
+class TestReports(openerp.tests.TransactionCase):
def test_reports(self):
+ return # commented out until post_install tests are working
+
registry, cr, uid = self.registry, self.cr, self.uid
r_model = registry('ir.actions.report.xml')
- domain = [('report_type','like','qweb')]
+ domain = [('report_type', 'like', 'qweb')]
for r in r_model.browse(cr, uid, r_model.search(cr, uid, domain)):
+ report_model = 'report.%s' % r.report_name
+ particular_model = registry('ir.model').search(cr, uid, [('model', '=', report_model)])
+
+ # Only test the generic reports here
+ if particular_model:
+ continue
+
+ _logger.info("testing report %s", r.report_name)
report_model = registry(r.model)
report_model_ids = report_model.search(cr, uid, [], limit=10)
if not report_model_ids:
_logger.info("no record found skipping report %s", r.report_name)
- continue
if not r.multi:
report_model_ids = report_model_ids[:1]
- url = "/report/%s/%s" % (r.report_name, ','.join(str(i) for i in report_model_ids))
- _logger.info("testing report %s", url)
- # TODO sle: uncomment this
- #content = self.url_open(url)
+ # Test report generation
+ registry('report').get_html(cr, uid, report_model_ids, r.report_name)
diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml
index 73c5ce03244..588775fc41e 100644
--- a/addons/report/views/layouts.xml
+++ b/addons/report/views/layouts.xml
@@ -4,9 +4,7 @@
<!DOCTYPE html>
diff --git a/addons/report/views/views.xml b/addons/report/views/views.xml
index c5d07f5287b..4c5f1b5092f 100644
--- a/addons/report/views/views.xml
+++ b/addons/report/views/views.xml
@@ -85,6 +85,7 @@
class="oe_link"
string="Search associated QWeb views"
name="associated_view"
+ attrs="{'invisible':[('report_type', 'not in', ['qweb-pdf', 'qweb-html'])]}"
/>
diff --git a/addons/report_webkit/default_header.html b/addons/report_webkit/default_header.html
index 0798209a5b7..37fd4135b62 100644
--- a/addons/report_webkit/default_header.html
+++ b/addons/report_webkit/default_header.html
@@ -1,3 +1,4 @@
+
diff --git a/addons/report_webkit/webkit_report.py b/addons/report_webkit/webkit_report.py
index 4f56d7efea7..60fb536ca96 100644
--- a/addons/report_webkit/webkit_report.py
+++ b/addons/report_webkit/webkit_report.py
@@ -112,6 +112,7 @@ def webkit_report_extender(report_name):
return fct
return fct1
+
class WebKitParser(report_sxw):
"""Custom class that use webkit to render HTML reports
Code partially taken from report openoffice. Thanks guys :)
@@ -173,7 +174,7 @@ class WebKitParser(report_sxw):
),
'w'
)
- head_file.write(header.encode('utf-8'))
+ head_file.write(self._sanitize_html(header.encode('utf-8')))
head_file.close()
file_to_del.append(head_file.name)
command.extend(['--header-html', head_file.name])
@@ -184,7 +185,7 @@ class WebKitParser(report_sxw):
),
'w'
)
- foot_file.write(footer.encode('utf-8'))
+ foot_file.write(self._sanitize_html(footer.encode('utf-8')))
foot_file.close()
file_to_del.append(foot_file.name)
command.extend(['--footer-html', foot_file.name])
@@ -205,7 +206,7 @@ class WebKitParser(report_sxw):
for html in html_list :
html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
count += 1
- html_file.write(html.encode('utf-8'))
+ html_file.write(self._sanitize_html(html.encode('utf-8')))
html_file.close()
file_to_del.append(html_file.name)
command.append(html_file.name)
@@ -366,7 +367,6 @@ class WebKitParser(report_sxw):
pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
return (pdf, 'pdf')
-
def create(self, cursor, uid, ids, data, context=None):
"""We override the create function in order to handle generator
Code taken from report openoffice. Thanks guys :) """
@@ -387,11 +387,18 @@ class WebKitParser(report_sxw):
report_xml.report_sxw = None
else:
return super(WebKitParser, self).create(cursor, uid, ids, data, context)
- if report_xml.report_type != 'webkit' :
+ if report_xml.report_type != 'webkit':
return super(WebKitParser, self).create(cursor, uid, ids, data, context)
result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
if not result:
return (False,False)
return result
+ def _sanitize_html(self, html):
+ """wkhtmltopdf expects the html page to declare a doctype.
+ """
+ if html and html[:9].upper() != "\n" + html
+ return html
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale/wizard/sale_make_invoice.py b/addons/sale/wizard/sale_make_invoice.py
index 3712feda20b..170259824f7 100644
--- a/addons/sale/wizard/sale_make_invoice.py
+++ b/addons/sale/wizard/sale_make_invoice.py
@@ -55,11 +55,12 @@ class sale_make_invoice(osv.osv_memory):
raise osv.except_osv(_('Warning!'), _("You shouldn't manually invoice the following sale order %s") % (sale_order.name))
order_obj.action_invoice_create(cr, uid, context.get(('active_ids'), []), data['grouped'], date_invoice=data['invoice_date'])
-
- for o in order_obj.browse(cr, uid, context.get(('active_ids'), []), context=context):
+ orders = order_obj.browse(cr, uid, context.get(('active_ids'), []), context=context)
+ for o in orders:
for i in o.invoice_ids:
newinv.append(i.id)
-
+ # Dummy call to workflow, will not create another invoice but bind the new invoice to the subflow
+ order_obj.signal_manual_invoice(cr, uid, [o.id for o in orders if o.order_policy == 'manual'])
result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
id = result and result[1] or False
result = act_obj.read(cr, uid, [id], context=context)[0]
diff --git a/addons/share/res_users.py b/addons/share/res_users.py
index 30200133f8e..702522478dd 100644
--- a/addons/share/res_users.py
+++ b/addons/share/res_users.py
@@ -38,9 +38,25 @@ class res_groups(osv.osv):
class res_users(osv.osv):
_name = 'res.users'
_inherit = 'res.users'
+
+ def _is_share(self, cr, uid, ids, name, args, context=None):
+ res = {}
+ for user in self.browse(cr, uid, ids, context=context):
+ res[user.id] = not self.has_group(cr, user.id, 'base.group_user')
+ return res
+
+ def _get_users_from_group(self, cr, uid, ids, context=None):
+ result = set()
+ for group in self.pool['res.groups'].browse(cr, uid, ids, context=context):
+ result.update(user.id for user in group.users)
+ return list(result)
+
_columns = {
- 'share': fields.boolean('Share User', readonly=True,
- help="External user with limited access, created only for the purpose of sharing data.")
+ 'share': fields.function(_is_share, string='Share User', type='boolean',
+ store={
+ 'res.users': (lambda self, cr, uid, ids, c={}: ids, None, 50),
+ 'res.groups': (_get_users_from_group, None, 50),
+ }, help="External user with limited access, created only for the purpose of sharing data."),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/share/share_data.xml b/addons/share/share_data.xml
index aba154a6403..c64a9099721 100644
--- a/addons/share/share_data.xml
+++ b/addons/share/share_data.xml
@@ -3,7 +3,6 @@
-
diff --git a/addons/share/wizard/share_wizard.py b/addons/share/wizard/share_wizard.py
index 10937015dd6..551b08d02cf 100644
--- a/addons/share/wizard/share_wizard.py
+++ b/addons/share/wizard/share_wizard.py
@@ -258,7 +258,6 @@ class share_wizard(osv.TransientModel):
'name': new_user,
'email': new_user,
'groups_id': [(6,0,[group_id])],
- 'share': True,
'company_id': current_user.company_id.id,
'company_ids': [(6, 0, [current_user.company_id.id])],
}, context)
@@ -276,7 +275,6 @@ class share_wizard(osv.TransientModel):
'password': new_pass,
'name': new_login,
'groups_id': [(6,0,[group_id])],
- 'share': True,
'company_id': current_user.company_id.id,
'company_ids': [(6, 0, [current_user.company_id.id])],
}, context)
diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py
index 01ed6f2409a..cf4099ac3ba 100644
--- a/addons/stock/wizard/stock_change_product_qty.py
+++ b/addons/stock/wizard/stock_change_product_qty.py
@@ -33,6 +33,10 @@ class stock_change_product_qty(osv.osv_memory):
'prodlot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
'location_id': fields.many2one('stock.location', 'Location', required=True, domain="[('usage', '=', 'internal')]"),
}
+ _defaults = {
+ 'new_quantity': 1,
+ 'product_id': lambda self, cr, uid, ctx: ctx and ctx.get('active_id', False) or False
+ }
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
if context is None: context = {}
@@ -54,20 +58,22 @@ class stock_change_product_qty(osv.osv_memory):
@param context: A standard dictionary
@return: A dictionary which of fields with values.
"""
- product_id = context and context.get('active_id', False) or False
+
res = super(stock_change_product_qty, self).default_get(cr, uid, fields, context=context)
- if 'new_quantity' in fields:
- res.update({'new_quantity': 1})
- if 'product_id' in fields:
- res.update({'product_id': product_id})
if 'location_id' in fields:
- try:
- model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
- self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
- except (orm.except_orm, ValueError):
- location_id = False
- res.update({'location_id': location_id})
+ location_id = res.get('location_id', False)
+ if not location_id:
+ try:
+ model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
+ except (orm.except_orm, ValueError):
+ pass
+ if location_id:
+ try:
+ self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
+ except (orm.except_orm, ValueError):
+ pass
+ res['location_id'] = location_id
return res
def change_product_qty(self, cr, uid, ids, context=None):
diff --git a/addons/stock/wizard/stock_move_view.xml b/addons/stock/wizard/stock_move_view.xml
index 5dc0d0d824c..f2bc40d9a3f 100644
--- a/addons/stock/wizard/stock_move_view.xml
+++ b/addons/stock/wizard/stock_move_view.xml
@@ -67,6 +67,7 @@
form
form
new
+ {'form_view_ref': False}
@@ -124,6 +125,7 @@
form
form
new
+ {'form_view_ref': False}
diff --git a/addons/subscription/__openerp__.py b/addons/subscription/__openerp__.py
index b301525eb42..5f8a1d1338b 100644
--- a/addons/subscription/__openerp__.py
+++ b/addons/subscription/__openerp__.py
@@ -37,7 +37,7 @@ e.g. To have an invoice generated automatically periodically:
above. Specify the interval information and partner to be invoice.
""",
'author': 'OpenERP SA',
- 'depends': [],
+ 'depends': ['base'],
'data': ['security/subcription_security.xml', 'security/ir.model.access.csv', 'subscription_view.xml'],
'demo': ['subscription_demo.xml',],
'installable': True,
diff --git a/addons/survey/wizard/survey_send_invitation.py b/addons/survey/wizard/survey_send_invitation.py
index 44d3bec7f6e..e16d0d6ce77 100644
--- a/addons/survey/wizard/survey_send_invitation.py
+++ b/addons/survey/wizard/survey_send_invitation.py
@@ -128,23 +128,14 @@ Thanks,''') % (name, self.pool.get('ir.config_parameter').get_param(cr, uid, 'we
for use in exist_user:
new_user.append(use.id)
for id in survey_ref.browse(cr, uid, survey_ids):
- report = self.create_report(cr, uid, [id.id], 'report.survey.form', id.title)
- file = open(get_module_resource('survey', 'report') + id.title +".pdf")
- file_data = ""
- while 1:
- line = file.readline()
- file_data += line
- if not line:
- break
- file.close()
- attachments[id.title +".pdf"] = file_data
- os.remove(get_module_resource('survey', 'report') + id.title +".pdf")
+ result, format = openerp.report.render_report(cr, uid, [id.id], 'survey.form', {}, {})
+ attachments[id.title +".pdf"] = result
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids):
if not partner.email:
skipped+= 1
continue
- user = user_ref.search(cr, uid, [('login', "=", partner.email)])
+ user = user_ref.search(cr, uid, [('partner_id', "=", partner.id)])
if user:
if user[0] not in new_user:
new_user.append(user[0])
@@ -185,6 +176,8 @@ Thanks,''') % (name, self.pool.get('ir.config_parameter').get_param(cr, uid, 'we
if ans:
res_data = {'name': partner.name or _('Unknown'),
'login': partner.email,
+ 'email': partner.email,
+ 'partner_id': partner.id,
'password': passwd,
'address_id': partner.id,
'groups_id': [[6, 0, [group_id]]],
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 47c0853d6be..1833ce102db 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -91,7 +91,7 @@ class ir_http(orm.AbstractModel):
assert path is not None
except Exception:
return self._handle_exception(werkzeug.exceptions.NotFound())
- if path != request.httprequest.path:
+ if path != werkzeug.url_quote(request.httprequest.path):
return werkzeug.utils.redirect(path)
def _handle_exception(self, exception=None, code=500):
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index 64aad94c83e..ef60aefbeff 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -381,42 +381,8 @@ class RelativeDatetime(orm.AbstractModel):
class Contact(orm.AbstractModel):
_name = 'website.qweb.field.contact'
- _inherit = ['website.qweb.field', 'website.qweb.field.many2one']
+ _inherit = ['ir.qweb.field.contact', 'website.qweb.field.many2one']
- def from_html(self, cr, uid, model, column, element, context=None):
- # FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget?
- divs = element.xpath(".//div")
- for div in divs:
- if div != divs[0]:
- div.getparent().remove(div)
- return super(Contact, self).from_html(cr, uid, model, column, element, context=context)
-
- def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
- opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
-
- if not getattr(record, field_name):
- return None
-
- id = getattr(record, field_name).id
- field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
- value = werkzeug.utils.escape( field_browse.name_get()[0][1] )
-
- val = {
- 'name': value.split("\n")[0],
- 'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
- 'phone': field_browse.phone,
- 'mobile': field_browse.mobile,
- 'fax': field_browse.fax,
- 'city': field_browse.city,
- 'country_id': field_browse.country_id and field_browse.country_id.name_get()[0][1],
- 'email': field_browse.email,
- 'fields': opf,
- 'options': options
- }
-
- html = self.pool["ir.ui.view"].render(cr, uid, "website.contact", val, engine='website.qweb', context=context).decode('utf8')
-
- return ir_qweb.HTMLSafe(html)
def html_to_text(element):
""" Converts HTML content with HTML-specified line breaks (br, p, div, ...)
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 857b99d40d2..67781cc02a0 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -168,20 +168,9 @@ class view(osv.osv):
arch_no_whitespace = etree.fromstring(
etree.tostring(arch, encoding='utf-8'),
parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True))
- arch_pretty_indent_2 = etree.tostring(
+ return etree.tostring(
arch_no_whitespace, encoding='unicode', pretty_print=True)
- # pretty_print uses a fixed indent level of 2, we want an indent of 4,
- # double up leading spaces.
- def repl(m):
- indent = len(m.group(0)) / 2
- return u' ' * 4 * indent
- # FIXME: If py2.7 only, can use re.M in sub and don't have to do replacement line by line
- return u'\n'.join(
- re.sub(ur'^((?: )+)', repl, line)
- for line in arch_pretty_indent_2.split(u'\n')
- )
-
def save(self, cr, uid, res_id, value, xpath=None, context=None):
""" Update a view section. The view section may embed fields to write
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js
index c89bf9ec2fb..ae126ad16ab 100644
--- a/addons/website/static/src/js/website.editor.js
+++ b/addons/website/static/src/js/website.editor.js
@@ -1789,6 +1789,9 @@
if (node.nodeName === 'BR' && node.getAttribute('type') === '_moz') {
//
appears when focusing RTE in FF, ignore
continue;
+ } else if (node.nodeName === 'DIV' && $(node).hasClass('oe_drop_zone')) {
+ // ignore dropzone inserted by snippets
+ continue
}
}
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index 1eee1fee65b..52deeabf5e6 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -1,6 +1,136 @@
(function () {
'use strict';
+/* Building block / Snippet Editor
+
+ The building blocks appear in the edit bar website. These prebuilt html block
+ allowing the designer to easily generate content on a page (drag and drop).
+ Options allow snippets to add customizations part html code according to their
+ selector (jQuery) and javascript object.
+
+ How to create content?
+
+ Designers can add their own html block in the "snippets" (/website/views/snippets.xml).
+ The block must be added in one of four menus (structure, content, feature or effect).
+ Structure:
+
+
+

+
...Block Name...
+
+
+ ...
+
+
+
+
+ How to create options?
+
+ Designers can add their own html block in the "snippet_options" (/website/views/snippets.xml).
+ Structure:
+
+
+ data-selector="..."
+ data-selector-siblings="..."
+ data-selector-children="..."
+ data-selector-vertical-children='...'>
+ ...
+
...
+ ...
+
+
+
+ How to create a javascript object for an options?
+
+ openerp.website.snippet.options["...option-id..."] = website.snippet.Option.extend({
+ // start is called when the user click into a block or when the user drop a block
+ // into the page (just after the init method).
+ // start is usually used to bind event.
+ //
+ // this.$target: block html inserted inside the page
+ // this.$el: html li list of this options
+ // this.$overlay: html editor overlay who content resize bar, customize menu...
+ start: function () {},
+
+
+ // onFocus is called when the user click inside the block inserted in page
+ // and when the user drop on block into the page
+ onFocus : function () {},
+
+
+ // onBlur is called when the user click outside the block inserted in page, if
+ // the block is focused
+ onBlur : function () {},
+
+
+ // on_clone is called when the snippet is duplicate
+ // @variables: $clone is allready inserted is the page
+ on_clone: function ($clone) {},
+
+
+ // on_remove is called when the snippet is removed (dom is removing after this tigger)
+ on_remove: function () {},
+
+
+ // drop_and_build_snippet is called just after that a thumbnail is drag and dropped
+ // into a drop zone. The content is already inserted in the page.
+ drop_and_build_snippet: function () {},
+
+ // select is called when a user select an item in the li list of options
+ // By default, if the li item have a data-value attribute, the data-vlue it's apply
+ // like a class to the html block (this.$target)
+ // @variables: next_previous = {$next, $prev}
+ // $next = next item selected or false
+ // $prev = previous item selected or false
+ select: function (event, next_previous) {}
+
+ // preview is called when a user is on mouse over or mouse out of an item
+ // variables: next_previous = {$next, $prev}
+ // $next = next item selected or false
+ // $prev = previous item selected or false
+ preview: function (event, next_previous) {}
+
+ // clean_for_save
+ // clean_for_save is called just before to save the vue
+ // Sometime it's important to remove or add some datas (contentEditable, added
+ // classes to a running animation...)
+ clean_for_save: function () {}
+ });
+
+
+ // 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped
+ // 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated
+
+*/
+
+
var website = openerp.website;
website.add_template_file('/website/static/src/xml/website.snippets.xml');
@@ -19,23 +149,11 @@
edit: function () {
var self = this;
$("[data-oe-model] *, [data-oe-type=html] *").off('click');
- website.snippet.stop_animation();
window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
this.snippets.appendTo(this.$el);
this.on('rte:ready', this, function () {
self.snippets.$button.removeClass("hidden");
- website.snippet.start_animation();
- $(website.snippet.readyAnimation).each(function() {
- var animation = $(this).data("snippet-view");
- if (animation) {
- animation.$target.on('focus', '*', function(){
- animation.stop();
- });
- animation.$target.on('blur', '*', function(){
- animation.start();
- });
- }
- });
+ website.snippet.stop_animation();
});
return this._super.apply(this, arguments);
@@ -56,9 +174,6 @@
}
});
- // 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped
- // 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated
-
if (!website.snippet) website.snippet = {};
website.snippet.templateOptions = {};
website.snippet.globalSelector = "";
@@ -397,18 +512,17 @@
$("#oe_snippets").trigger('snippet-dropped', $target);
website.snippet.start_animation(true, $target);
-
// drop_and_build_snippet
self.create_overlay($target);
if ($target.data("snippet-editor")) {
- $target.data("snippet-editor").drop_and_build_snippet($target);
+ $target.data("snippet-editor").drop_and_build_snippet();
}
for (var k in website.snippet.templateOptions) {
$target.find(website.snippet.templateOptions[k].selector).each(function () {
var $snippet = $(this);
self.create_overlay($snippet);
if ($snippet.data("snippet-editor")) {
- $snippet.data("snippet-editor").drop_and_build_snippet($snippet);
+ $snippet.data("snippet-editor").drop_and_build_snippet();
}
});
}
@@ -665,80 +779,12 @@
self.set_active();
self.$target.trigger("snippet-style-change", [self, np]);
},0);
- this.select(event, {'$next': $next, '$prev': $prev});
+ this.select({'$next': $next, '$prev': $prev});
} else {
setTimeout(function () {
self.$target.trigger("snippet-style-preview", [self, np]);
},0);
- this.preview(event, np);
- }
- },
- // start is call just after the init
- start: function () {
- },
- /* onFocus
- * This method is called when the user click inside the snippet in the dom
- */
- onFocus : function () {
- },
-
- /* onFocus
- * This method is called when the user click outside the snippet in the dom, after a focus
- */
- onBlur : function () {
- },
-
- /* on_clone
- * This method is called when the snippet is cloned ($clone is allready inserted)
- */
- on_clone: function ($clone) {
- },
-
- /* on_remove
- * This method is called when the snippet is removed (dom is removing after this call)
- */
- on_remove: function () {
- },
-
- /*
- * drop_and_build_snippet
- * This method is called just after that a thumbnail is drag and dropped into a drop zone
- * (after the insertion of this.$body, if this.$body exists)
- */
- drop_and_build_snippet: function ($target) {
- },
- /* select
- * called when a user select an item
- * li must have data-value attribute
- * variables: np = {$next, $prev}
- * $next is false if they are no next item selected
- * $prev is false if they are no previous item selected
- */
- select: function (event, np) {
- var self = this;
- // add or remove html class
- if (np.$prev) {
- this.$target.removeClass(np.$prev.data('value' || ""));
- }
- if (np.$next) {
- this.$target.addClass(np.$next.data('value') || "");
- }
- },
- /* preview
- * called when a user is on mouse over or mouse out of an item
- * variables: np = {$next, $prev}
- * $next is false if they are no next item selected
- * $prev is false if they are no previous item selected
- */
- preview: function (event, np) {
- var self = this;
-
- // add or remove html class
- if (np.$prev) {
- this.$target.removeClass(np.$prev.data('value') || "");
- }
- if (np.$next) {
- this.$target.addClass(np.$next.data('value') || "");
+ this.preview(np);
}
},
/* set_active
@@ -757,9 +803,48 @@
.addClass("active");
this.$el.find('li:has(li[data-value].active)').addClass("active");
},
- /* clean_for_save
- * function called just before save vue
- */
+
+ start: function () {
+ },
+
+ onFocus : function () {
+ },
+
+ onBlur : function () {
+ },
+
+ on_clone: function ($clone) {
+ },
+
+ on_remove: function () {
+ },
+
+ drop_and_build_snippet: function () {
+ },
+
+ select: function (np) {
+ var self = this;
+ // add or remove html class
+ if (np.$prev && this.required) {
+ this.$target.removeClass(np.$prev.data('value' || ""));
+ }
+ if (np.$next) {
+ this.$target.addClass(np.$next.data('value') || "");
+ }
+ },
+
+ preview: function (np) {
+ var self = this;
+
+ // add or remove html class
+ if (np.$prev && this.required) {
+ this.$target.removeClass(np.$prev.data('value') || "");
+ }
+ if (np.$next) {
+ this.$target.addClass(np.$next.data('value') || "");
+ }
+ },
+
clean_for_save: function () {
}
});
@@ -776,9 +861,9 @@
var src = this._get_bg();
this.$el.find("li[data-value].active.oe_custom_bg").data("src", src);
},
- select: function(event, np) {
+ select: function(np) {
var self = this;
- this._super(event, np);
+ this._super(np);
if (np.$next) {
if (np.$next.hasClass("oe_custom_bg")) {
var editor = new website.editor.ImageDialog();
@@ -803,8 +888,8 @@
this.$target.removeClass(np.$prev.data("value"));
}
},
- preview: function (event, np) {
- this._super(event, np);
+ preview: function (np) {
+ this._super(np);
if (np.$next) {
this._set_bg(np.$next.data("src"));
}
@@ -872,6 +957,7 @@
start : function () {
var self = this;
this._super();
+ this.$target.carousel({interval: false});
this.id = this.$target.attr("id");
this.$inner = this.$target.find('.carousel-inner');
this.$indicators = this.$target.find('.carousel-indicators');
@@ -1539,9 +1625,9 @@
* This method is called just after that a thumbnail is drag and dropped into a drop zone
* (after the insertion of this.$body, if this.$body exists)
*/
- drop_and_build_snippet: function ($target) {
+ drop_and_build_snippet: function () {
for (var i in this.styles){
- this.styles[i].drop_and_build_snippet($target);
+ this.styles[i].drop_and_build_snippet();
}
},
diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml
index 55bd7679b66..eb1f8c988d7 100644
--- a/addons/website/views/snippets.xml
+++ b/addons/website/views/snippets.xml
@@ -923,7 +923,7 @@
+ data-selector=".carousel:not(.quotecarousel)">