diff --git a/addons/account/res_config.py b/addons/account/res_config.py
index bf28e900378..8980a31927e 100644
--- a/addons/account/res_config.py
+++ b/addons/account/res_config.py
@@ -105,6 +105,9 @@ class account_config_settings(osv.osv_memory):
'module_account_followup': fields.boolean('Manage customer payment follow-ups',
help='This allows to automate letters for unpaid invoices, with multi-level recalls.\n'
'-This installs the module account_followup.'),
+ 'module_product_email_template': fields.boolean('Send products tools and information at the invoice confirmation',
+ help='With this module, link your products to a template to send complete information and tools to your customer.\n'
+ 'For instance when invoicing a training, the training agenda and materials will automatically be send to your customers.'),
'group_proforma_invoices': fields.boolean('Allow pro-forma invoices',
implied_group='account.group_proforma_invoices',
help="Allows you to put invoices in pro-forma state."),
diff --git a/addons/account/res_config_view.xml b/addons/account/res_config_view.xml
index ce8b41fc2da..103344daa3a 100644
--- a/addons/account/res_config_view.xml
+++ b/addons/account/res_config_view.xml
@@ -183,6 +183,10 @@
+
+
+
+
diff --git a/addons/base_action_rule/__openerp__.py b/addons/base_action_rule/__openerp__.py
index 973567c613f..5dd4b057514 100644
--- a/addons/base_action_rule/__openerp__.py
+++ b/addons/base_action_rule/__openerp__.py
@@ -35,7 +35,7 @@ trigger an automatic reminder email.
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
- 'depends': ['base', 'mail'],
+ 'depends': ['base', 'resource', 'mail'],
'data': [
'base_action_rule_view.xml',
'security/ir.model.access.csv',
diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py
index 0d98030e800..58728a77aef 100644
--- a/addons/base_action_rule/base_action_rule.py
+++ b/addons/base_action_rule/base_action_rule.py
@@ -78,6 +78,11 @@ class base_action_rule(osv.osv):
"trigger date, like sending a reminder 15 minutes before a meeting."),
'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'),
('day', 'Days'), ('month', 'Months')], 'Delay type'),
+ 'trg_date_calendar_id': fields.many2one(
+ 'resource.calendar', 'Use Calendar',
+ help='When calculating a day-based timed condition, it is possible to use a calendar to compute the date based on working days.',
+ ondelete='set null',
+ ),
'act_user_id': fields.many2one('res.users', 'Set Responsible'),
'act_followers': fields.many2many("res.partner", string="Add Followers"),
'server_action_ids': fields.many2many('ir.actions.server', string='Server Actions',
@@ -241,6 +246,18 @@ class base_action_rule(osv.osv):
data.update({'model': model.model})
return {'value': data}
+ def _check_delay(self, cr, uid, action, record, record_dt, context=None):
+ if action.trg_date_calendar_id and action.trg_date_range_type == 'day':
+ start_dt = get_datetime(record_dt)
+ action_dt = self.pool['resource.calendar'].schedule_days_get_date(
+ cr, uid, action.trg_date_calendar_id.id, action.trg_date_range,
+ day_date=start_dt, compute_leaves=True, context=context
+ )
+ else:
+ delay = DATE_RANGE_FUNCTION[action.trg_date_range_type](action.trg_date_range)
+ action_dt = get_datetime(record_dt) + delay
+ return action_dt
+
def _check(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
""" This Function is called by scheduler. """
context = context or {}
@@ -267,14 +284,12 @@ class base_action_rule(osv.osv):
else:
get_record_dt = lambda record: record[date_field]
- delay = DATE_RANGE_FUNCTION[action.trg_date_range_type](action.trg_date_range)
-
# process action on the records that should be executed
for record in model.browse(cr, uid, record_ids, context=context):
record_dt = get_record_dt(record)
if not record_dt:
continue
- action_dt = get_datetime(record_dt) + delay
+ action_dt = self._check_delay(cr, uid, action, record, record_dt, context=context)
if last_run and (last_run <= action_dt < now) or (action_dt < now):
try:
self._process(cr, uid, action, [record.id], context=context)
diff --git a/addons/base_action_rule/base_action_rule_view.xml b/addons/base_action_rule/base_action_rule_view.xml
index c2262adde39..a4341d403ca 100644
--- a/addons/base_action_rule/base_action_rule_view.xml
+++ b/addons/base_action_rule/base_action_rule_view.xml
@@ -43,6 +43,8 @@
+
diff --git a/addons/product_email_template/__init__.py b/addons/product_email_template/__init__.py
new file mode 100644
index 00000000000..c7b5ac7e1f2
--- /dev/null
+++ b/addons/product_email_template/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import models
diff --git a/addons/product_email_template/__openerp__.py b/addons/product_email_template/__openerp__.py
new file mode 100644
index 00000000000..2fdce7f2889
--- /dev/null
+++ b/addons/product_email_template/__openerp__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Product Email Template',
+ 'depends': ['account'],
+ 'author': 'OpenERP SA',
+ 'category': 'Accounting & Finance',
+ 'description': """
+Add email templates to products to be send on invoice confirmation
+==================================================================
+
+With this module, link your products to a template to send complete information and tools to your customer.
+For instance when invoicing a training, the training agenda and materials will automatically be send to your customers.'
+ """,
+ 'website': 'http://www.openerp.com',
+ 'demo': [
+ 'data/product_demo.xml',
+ ],
+ 'data': [
+ 'views/product_view.xml',
+ 'views/email_template_view.xml',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/addons/product_email_template/data/product_demo.xml b/addons/product_email_template/data/product_demo.xml
new file mode 100644
index 00000000000..bc5290fbd17
--- /dev/null
+++ b/addons/product_email_template/data/product_demo.xml
@@ -0,0 +1,648 @@
+
+
+
+
+ Online Training
+ Online Training
+
+
+
+ Online Training + Certification
+
+
+ These courses feature the same high quality course content found in our traditional classroom trainings, supplemented with
+ modular sessions and cloud-based labs. Many of our online learning courses also include dozens of recorded webinars and live
+ sessions by our senior instructors. At the end of the training, you can pass the OpenERP Certification exam in one of the 5000+
+ Pearson VUE test centers worldwide.
+
+
+ Your advantages
+
+
Modular approach applied to the learning method
+
New interactive learning experience
+
Lower training budget for the same quality courses
+
Better comfort to facilitate your learning process
+
OpenERP Certification Exam included (Pre-Ordered, available as of November 1st, 2013)
+
+
+
+
+ Structure of the Training
+
+
+
+
+
+
+
+
+ There are three components to the training
+
+
Videos with detailed demos
+
Hands-on exercises and their solutions
+
Live Q&A sessions with a trainer
+
+
+
+
+
+
+ The 5-day training is modular, which means that you can choose to participate in the full training or to just 1 or 2 modules.
+ Nevertheless, the first day of the training is compulsory for everyone. The Monday is compulsory because the
+ introduction of OpenERP is important before going through the other modules.
+ Each day of the week starts from 9 AM (CEST/PST) to 12 PM (CEST/PST). A Q&A session will be hosted by an OpenERP trainer.
+ Each day, the participants are expected to have done the following BEFORE attending the Q&A session:
+
+
Watch the videos for this day
+
Do the related exercises (written solutions will be provided upfront as well)
+
Send their questions by email before 5PM (CEST/PST) on the previous business day
+
+ The Q&A session will be hosted on a web conference service. A stable internet connection is required. The trainer will
+ screen the questions for the day and select the most relevant ones to be answered during the session. The purpose of the
+ Q&A session is not to cover questions related to a specific implementation project or company-specific business
+ workflows. The questions should be about the material covered in the videos and the exercises and should benefit
+ the other participants of the training as well.
+ Each day of the week is dedicated to specific applications in OpenERP as per the following planning:
+
Social network features: emailing, internal communication
+
+
+
+ The introduction exercises have been created to help you discover OpenERP in an easy way. Their main objective is to present the different applications, menus, view types, shortcuts, field types, wizards, actions, etc. It will help you to discover how to navigate in OpenERP!
+ Therefore, the Introduction is compulsory for everyone, even if you need to learn a specific module given on an other date. This obligation has been made to make sure that you are not lost if you arrive in the middle of the training for another module.
+
+
+
+
CRM Module
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Manage Lead & Opportunities
+
Call, Meeting
+
Partners & contacts
+
Automated actions
+
Import partners and contacts from LinkedIn
+
+
+
+
+
Track all leads coming in and push them to real opportunities
+
Improve the productivity of the sales force
+
Access all documents and messages related to one lead/opportunity in one place
+
Track the sales pipeline by month and stage to produce
+ accurate sales forecasts at the individual and group
+ levels
+
Track individual performance of sales people
+
+
+
+
+
Sales Module
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Customers
+
Products to sell
+
Manual discounts
+
Margins
+
Sales & Warehouse: Invoicing control (On demand / On
+ Delivery Order), Shipping policy (At once / Each product
+ when available)
+
+
+
+
+
Register orders from customers and create invoices
+
Schedule deliveries (for products) or tasks (for service)
+
Ensure that sales orders are properly invoiced by tracking pending sales orders
+
+
+
+
+
+ CRM & Sales management:
+
+
From an opportunity to a sale order
+
Claims
+
Aftersale communication
+
+
+
+ OpenERP can work with independent applications, so the
+ whole commercial process can be integrated together in
+ order to accomplish complete business flows.
+
+ Thanks to the integrated access rights management integrated, each user has an access to its relevant information. No overloaded screen with a lot of irrelevant data, no unwanted menu, etc. Only a clean interface!
+
+
+
+
Purchase
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Suppliers
+
Products
+
Request for Quotation
+
Purchase order
+
Invoicing & Invoice control
+
Incoming order (Complete/partial reception)
+
Purchase requisition
+
+
+
+
+
Better negotiate volumes and prices based on historical data
+
Centralize all the data to be able to build reporting and statistics
+
Control the invoicing process
+
Work with purchase requisitions to ask different suppliers to submit quotations
+
+
+
+
+
+ Sales & Purchase management
+
+
Make to order product
+
Retailer process: buy a product and sell it without any customization/manufacturing
+
+
+
+ Keep control of the replenishment process based on sales demands! Thanks to this integration, you will be able to automate the ordering for the missing units ordered by a customer
+
+
+
+
Financial accounting
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Invoices
+
Refunds
+
Journal Entries & Reconciliations
+
Reports / Closing of fiscal year
+
Accounts
+
+
+
+
+
Rapidly encode your financial operations or accounting transactions
+
Carry out payments easily and adequately reconcile these payments with invoices
+
Quick creation of invoices with pre-set defaults on debtor/creditor and income/expense accounts
+
Multiple manners to reconcile
+
Configuration of accounts to ensure correct display in balance sheet and profit & loss statement
+
Apply correct deferral methods to ensure the close of the fiscal year
+
+
+
+
+
+
+ Wednesday
+ Project management, Human resources, Contract management
+
Project Management
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Projects
+
Tasks and tasks delegation
+
Issues and issues escalation
+
+
+
+
+
Manage and follow your projects, tasks and issues
+
Discover the Kanban view to quickly operate some actions and to quickly detect any bottlenecks in your projects flows
+
Discover the Gantt view to see how your project has been planned and adapt it according to new elements or to the people agenda
+
Define project members & delegate tasks
+
Register issues and escalate them
+
Create automatically timesheet lines in the Human Resources menu from recorded work activities
+
Track and invoice (to customers) the costs associated to a project
+
Manage a support ticket system
+
+
+
+
+
Human Resources
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Employees
+
Recruitment
+
Expenses
+
Allocations and leaves requests
+
Time management: timesheet and timesheet lines
+
+
+
+ About Recruiting:
+
+
Better negotiate volumes and prices based on historical data
+
Centralize all the data to be able to build reporting and statistics
+
Control the invoicing process
+
Work with purchase requisitions to ask different suppliers to submit quotations
+
+ About Holidays:
+
+
Track the number of vacation days accrued by each employee
+
Allow managers to approve leave requests and plan backups for their teams
+
+
+
+
+
Contract management
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Contracts
+
Invoicing methods
+
Sale orders
+
Timesheets
+
Expenses
+
+
+
+
+
+
Centralize the data related to a specific contract in one single point
+
Perform the invoicing from one single place (Sales order / Timesheet / Expenses)
+ OpenERP Warehouse Management is at once very simple, flexible and complete. It is based on the concept of double entry that revolutionized accounting: ‘Nothing lost, everything moved’. In OpenERP, we don’t talk about disappearance, consumption or loss of products: instead we speak only of stock moves from one place to another.
+
+
+
+
Manufacturing (MRP)
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Bill of materials
+
Multi-level BoM
+
Routings
+
Work center operations
+
+
+
+
+
Manage the production planning
+
Organize the different manufacturing orders to optimize the workload of the different resources
+
From an operational perspective, track which products and quantities need to be manufactured
+
+ As a manager:
+
+
Define products to be assembled or to be sold as a kit
+
Organize the pipeline of the production
+
Track the cost of a manufacturing order and measure the efficiency of the department
+
+
+
+
+
+ MRP & Sales
+
+
Product configuration Make to Stock / to Order
+
Commercial BoM (kit)
+
Just in Time
+
+
+
+ From the sales perspective, you will have a manufacturing order generated based on a request coming from a customer without any manual operation. This integration of those two aplications allows you to perform the following flow in an automated way: Sale order –> Stock level verification –> Production of the missing units –> Reception in stock of the finished goods –> Delivery to the customer.
+
+
+
+
Import/Export
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Import from CSV files
+
Export as CSV or as Excel file
+
+
+
+
+
Transfer data from one system to OpenERP.
+
+ OpenERP offers a simple way to import and export data. OpenERP will guide you for this phase thanks to a FAQ directly integrated into the import screen
+
+
+
+
+
+ Friday
+ Pricelists, Point of Sale (POS), Introduction to report customization
+
Pricelists
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Prices for returning customers
+
Prices in foreign currencies (with exchange fees)
+
Retailer prices (based on a minimal margin)
+
+
+
+
+
Keep control on the pricing policy
+
Allow discounts for some periods
+
Ensure your margins even when you define discounts
+
Allow different pricing by customer and/or supplier
+
Update the prices in an easy way
+
+
+
+
+
Point of Sale (POS)
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Payment methods (journals)
+
POS Configuration
+
POS front-end
+
POS back-end
+
Shops
+
Sessions
+
POS Orders
+
Re-Invoicing
+
Scanning / Self-scanning
+
+
+
+ OpenERP POS is the first POS running in a 100% web based environment, which means any computer with a browser can host the POS. The POS is a two-part system with a front-end (interaction with the client) and a back-end (managers can configure the system, print reports, analyse, ...)
+ About the front-end:
+
+
Offline mode. Imagine several cases were having an offline mode is of prime interest: Connexion to server shutdown at a big supermarket / Use in environments that requires mobility but without wifi / Use in large environments like restaurants and gardens / Use in low tech environments
+
+ About the back-end:
+
+
Configure the products being sold in the POS
+
Configure the payments methods
+
Print daily sales report
+
Analyze sales
+
+
+
+
+
Introduction to report customization
+
+
+
Content
+
What will you learn?
+
+
+
+
+
Customize the layout of your invoices
+
Change the footer/header of documents
+
Add a field in a standard document / report
+
+
+
+ This features will help you to fully customize the standard documents made from OpenERP.
+
+ As soon as registration will be finalized (ie. payment confirmation), the participants will be provided with the material listed
+ below:
+
+
Access to online videos with detailed demos
+
Training material with exercises
+
Written solutions to the exercises
+
+ Therefore, there is an advantage to registering early to the training if you are interested in preparing your live sessions ahead of time. Moreover, the number of participants is limited per online session which is another reason to consider registering early and securing your seat.
+
+
+
+
+
+ With the live sessions, we learned a lot more than what was covered in the videos. Definitely a good start for building up a solid base of working knowledge of OpenERP.
+
+
+ The trainer did a wonderful job with the training! He was well informed and interactive online and offline. Thank you so much for the experience!
+
+
+
+
+ It would be very difficult to cover all of the possibilities of OpenERP and I believe the major aspects were covered very well
+
+
+ The trainer has a very good knowledge of the subject, he understands the issues of the trainees very fast and provided answers with the right level of illustration.
+
+
+
+
+
+ ]]>
+
+
+
+
+ Online Training
+
+ 500
+ 900
+ service
+
+
+
+
+
+
+
diff --git a/addons/product_email_template/models/__init__.py b/addons/product_email_template/models/__init__.py
new file mode 100644
index 00000000000..2edb825574d
--- /dev/null
+++ b/addons/product_email_template/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+import product
+import invoice
diff --git a/addons/product_email_template/models/invoice.py b/addons/product_email_template/models/invoice.py
new file mode 100644
index 00000000000..d9e82e42012
--- /dev/null
+++ b/addons/product_email_template/models/invoice.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import osv
+
+
+class account_invoice(osv.Model):
+ _inherit = 'account.invoice'
+
+ def invoice_validate_send_email(self, cr, uid, ids, context=None):
+ Composer = self.pool['mail.compose.message']
+ for invoice in self.browse(cr, uid, ids, context=context):
+ # send template only on customer invoice
+ if invoice.type != 'out_invoice':
+ continue
+ # subscribe the partner to the invoice
+ if invoice.partner_id.id not in invoice.message_follower_ids:
+ self.message_subscribe(cr, uid, [invoice.id], [invoice.partner_id.id], context=context)
+ for line in invoice.invoice_line:
+ if line.product_id.email_template_id:
+ # CLEANME: should define and use a clean API: message_post with a template
+ composer_id = Composer.create(cr, uid, {
+ 'model': 'account.invoice',
+ 'res_id': invoice.id,
+ 'template_id': line.product_id.email_template_id.id,
+ 'composition_mode': 'comment',
+ }, context=context)
+ template_values = Composer.onchange_template_id(
+ cr, uid, composer_id, line.product_id.email_template_id.id, 'comment', 'account.invoice', invoice.id
+ )['value']
+ template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', '[]')]
+ Composer.write(cr, uid, [composer_id], template_values, context=context)
+ Composer.send_mail(cr, uid, [composer_id], context=context)
+ return True
+
+ def invoice_validate(self, cr, uid, ids, context=None):
+ res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
+ self.invoice_validate_send_email(cr, uid, ids, context=context)
+ return res
diff --git a/addons/product_email_template/models/product.py b/addons/product_email_template/models/product.py
new file mode 100644
index 00000000000..cafdef7ab9d
--- /dev/null
+++ b/addons/product_email_template/models/product.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, osv
+
+
+class product_template(osv.Model):
+ """ Product Template inheritance to add an optional email.template to a
+ product.template. When validating an invoice, an email will be send to the
+ customer based on this template. The customer will receive an email for each
+ product linked to an email template. """
+ _inherit = "product.template"
+
+ _columns = {
+ 'email_template_id': fields.many2one(
+ 'email.template', 'Product Email Template',
+ help='When validating an invoice, an email will be send to the customer'
+ 'based on this template. The customer will receive an email for each'
+ 'product linked to an email template.'),
+ }
diff --git a/addons/product_email_template/views/email_template_view.xml b/addons/product_email_template/views/email_template_view.xml
new file mode 100644
index 00000000000..5b4600b9719
--- /dev/null
+++ b/addons/product_email_template/views/email_template_view.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ email.template.form.simplified
+ email.template
+ 100
+
+
+
+
+
+
diff --git a/addons/product_email_template/views/product_view.xml b/addons/product_email_template/views/product_view.xml
new file mode 100644
index 00000000000..4d036fb8de0
--- /dev/null
+++ b/addons/product_email_template/views/product_view.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ product.normal.procurement.locations.inherit
+ product.product
+
+
+
+
+
+
+
+
+
diff --git a/addons/resource/resource.py b/addons/resource/resource.py
index 212a7dd6828..cbaf15147d1 100644
--- a/addons/resource/resource.py
+++ b/addons/resource/resource.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
+# Copyright (C) 2004-TODAY OpenERP SA (http://www.openerp.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -19,304 +19,613 @@
#
##############################################################################
-import pytz
-from datetime import datetime, timedelta
+import datetime
from dateutil import rrule
-import math
+from dateutil.relativedelta import relativedelta
+from operator import itemgetter
+
from faces import *
+from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.float_utils import float_compare
from openerp.tools.translate import _
-from itertools import groupby
-from operator import itemgetter
-
class resource_calendar(osv.osv):
+ """ Calendar model for a resource. It has
+
+ - attendance_ids: list of resource.calendar.attendance that are a working
+ interval in a given weekday.
+ - leave_ids: list of leaves linked to this calendar. A leave can be general
+ or linked to a specific resource, depending on its resource_id.
+
+ All methods in this class use intervals. An interval is a tuple holding
+ (begin_datetime, end_datetime). A list of intervals is therefore a list of
+ tuples, holding several intervals of work or leaves. """
_name = "resource.calendar"
_description = "Resource Calendar"
+
_columns = {
- 'name' : fields.char("Name", size=64, required=True),
- 'company_id' : fields.many2one('res.company', 'Company', required=False),
- 'attendance_ids' : fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
- 'manager' : fields.many2one('res.users', 'Workgroup Manager'),
+ 'name': fields.char("Name", size=64, required=True),
+ 'company_id': fields.many2one('res.company', 'Company', required=False),
+ 'attendance_ids': fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
+ 'manager': fields.many2one('res.users', 'Workgroup Manager'),
+ 'leave_ids': fields.one2many(
+ 'resource.calendar.leaves', 'calendar_id', 'Leaves',
+ help=''
+ ),
}
_defaults = {
'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
}
- def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
- """Calculates the Working Total Hours based on Resource Calendar and
- given working day (datetime object).
+ # --------------------------------------------------
+ # Utility methods
+ # --------------------------------------------------
- @param resource_calendar_id: resource.calendar browse record
- @param day: datetime object
+ def interval_clean(self, intervals):
+ """ Utility method that sorts and removes overlapping inside datetime
+ intervals. The intervals are sorted based on increasing starting datetime.
+ Overlapping intervals are merged into a single one.
- @return: returns the working hours (as float) men should work on the given day if is in the attendance_ids of the resource_calendar_id (i.e if that day is a working day), returns 0.0 otherwise
- """
- res = 0.0
- for working_day in resource_calendar_id.attendance_ids:
- if (int(working_day.dayofweek) + 1) == day.isoweekday():
- res += working_day.hour_to - working_day.hour_from
- return res
+ :param list intervals: list of intervals; each interval is a tuple
+ (datetime_from, datetime_to)
+ :return list cleaned: list of sorted intervals without overlap """
+ intervals = sorted(intervals, key=itemgetter(0)) # sort on first datetime
+ cleaned = []
+ working_interval = None
+ while intervals:
+ current_interval = intervals.pop(0)
+ if not working_interval: # init
+ working_interval = [current_interval[0], current_interval[1]]
+ elif working_interval[1] < current_interval[0]: # interval is disjoint
+ cleaned.append(tuple(working_interval))
+ working_interval = [current_interval[0], current_interval[1]]
+ elif working_interval[1] < current_interval[1]: # union of greater intervals
+ working_interval[1] = current_interval[1]
+ if working_interval: # handle void lists
+ cleaned.append(tuple(working_interval))
+ return cleaned
- def _get_leaves(self, cr, uid, id, resource):
- """Private Method to Calculate resource Leaves days
+ def interval_remove_leaves(self, interval, leave_intervals):
+ """ Utility method that remove leave intervals from a base interval:
- @param id: resource calendar id
- @param resource: resource id for which leaves will ew calculated
+ - clean the leave intervals, to have an ordered list of not-overlapping
+ intervals
+ - initiate the current interval to be the base interval
+ - for each leave interval:
- @return : returns the list of dates, where resource on leave in
- resource.calendar.leaves object (e.g.['%Y-%m-%d', '%Y-%m-%d'])
- """
- resource_cal_leaves = self.pool.get('resource.calendar.leaves')
- dt_leave = []
- resource_leave_ids = resource_cal_leaves.search(cr, uid, [('calendar_id','=',id), '|', ('resource_id','=',False), ('resource_id','=',resource)])
- #res_leaves = resource_cal_leaves.read(cr, uid, resource_leave_ids, ['date_from', 'date_to'])
- res_leaves = resource_cal_leaves.browse(cr, uid, resource_leave_ids)
+ - finishing before the current interval: skip, go to next
+ - beginning after the current interval: skip and get out of the loop
+ because we are outside range (leaves are ordered)
+ - beginning within the current interval: close the current interval
+ and begin a new current interval that begins at the end of the leave
+ interval
+ - ending within the current interval: update the current interval begin
+ to match the leave interval ending
- for leave in res_leaves:
- dtf = datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S')
- dtt = datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S')
- no = dtt - dtf
- [dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
- dt_leave.sort()
-
- return dt_leave
-
- def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
- """
- Calculates the working Schedule from supplied from date to till hours
- will be satisfied based or resource calendar id. If resource is also
- given then it will consider the resource leave also and than will
- calculates resource working schedule
-
- @param dt_from: datetime object, start of working scheduled
- @param hours: float, total number working hours needed scheduled from
- start date
- @param resource : Optional Resource id, if supplied than resource leaves
- will also taken into consideration for calculating working
- schedule.
- @return : List datetime object of working schedule based on supplies
- params
- """
- if not id:
- td = int(hours)*3
- return [(dt_from - timedelta(hours=td), dt_from)]
- dt_leave = self._get_leaves(cr, uid, id, resource)
- dt_leave.reverse()
- todo = hours
- result = []
- maxrecur = 100
- current_hour = dt_from.hour
- while float_compare(todo, 0, 4) and maxrecur:
- cr.execute("select hour_from,hour_to from resource_calendar_attendance where dayofweek='%s' and calendar_id=%s order by hour_from desc", (dt_from.weekday(),id))
- for (hour_from,hour_to) in cr.fetchall():
- leave_flag = False
- if (hour_fromtodo:
- hour_from = m-todo
- dt_check = dt_from.strftime('%Y-%m-%d')
- for leave in dt_leave:
- if dt_check == leave:
- dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
- leave_flag = True
- if leave_flag:
- break
- else:
- d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60))
- d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
- result.append((d1, d2))
- current_hour = hour_from
- todo -= (m-hour_from)
- dt_from -= timedelta(days=1)
- current_hour = 24
- maxrecur -= 1
- result.reverse()
- return result
-
- # def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
- def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
- def group(lst, key):
- lst.sort(key=itemgetter(key))
- grouped = groupby(lst, itemgetter(key))
- return dict([(k, [v for v in itr]) for k, itr in grouped])
- # END group
-
- cr.execute("select calendar_id, dayofweek, hour_from, hour_to from resource_calendar_attendance order by hour_from")
- hour_res = cr.dictfetchall()
- hours_by_cal = group(hour_res, 'calendar_id')
-
- results = {}
-
- for d, hours, id in date_and_hours_by_cal:
- dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S')
- if not id:
- td = int(hours)*3
- results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))]
+ :param tuple interval: a tuple (beginning datetime, ending datetime) that
+ is the base interval from which the leave intervals
+ will be removed
+ :param list leave_intervals: a list of tuples (beginning datetime, ending datetime)
+ that are intervals to remove from the base interval
+ :return list intervals: a list of tuples (begin datetime, end datetime)
+ that are the remaining valid intervals """
+ if not interval:
+ return interval
+ if leave_intervals is None:
+ leave_intervals = []
+ intervals = []
+ leave_intervals = self.interval_clean(leave_intervals)
+ current_interval = [interval[0], interval[1]]
+ for leave in leave_intervals:
+ if leave[1] <= current_interval[0]:
continue
+ if leave[0] >= current_interval[1]:
+ break
+ if current_interval[0] < leave[0] < current_interval[1]:
+ current_interval[1] = leave[0]
+ intervals.append((current_interval[0], current_interval[1]))
+ current_interval = [leave[1], interval[1]]
+ # if current_interval[0] <= leave[1] <= current_interval[1]:
+ if current_interval[0] <= leave[1]:
+ current_interval[0] = leave[1]
+ if current_interval and current_interval[0] < interval[1]: # remove intervals moved outside base interval due to leaves
+ intervals.append((current_interval[0], current_interval[1]))
+ return intervals
- dt_leave = self._get_leaves(cr, uid, id, resource)
- todo = hours
- result = []
- maxrecur = 100
- current_hour = dt_from.hour
- while float_compare(todo, 0, 4) and maxrecur:
- for (hour_from,hour_to) in [(item['hour_from'], item['hour_to']) for item in hours_by_cal[id] if item['dayofweek'] == str(dt_from.weekday())]:
- leave_flag = False
- if (hour_to>current_hour) and float_compare(todo, 0, 4):
- m = max(hour_from, current_hour)
- if (hour_to-m)>todo:
- hour_to = m+todo
- dt_check = dt_from.strftime('%Y-%m-%d')
- for leave in dt_leave:
- if dt_check == leave:
- dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
- leave_flag = True
- if leave_flag:
- break
- else:
- d1 = datetime(dt_from.year, dt_from.month, dt_from.day) + timedelta(hours=int(math.floor(m)), minutes=int((m%1) * 60))
- d2 = datetime(dt_from.year, dt_from.month, dt_from.day) + timedelta(hours=int(math.floor(hour_to)), minutes=int((hour_to%1) * 60))
- result.append((d1, d2))
- current_hour = hour_to
- todo -= (hour_to - m)
- dt_from += timedelta(days=1)
- current_hour = 0
- maxrecur -= 1
- results[(d, hours, id)] = result
+ def interval_schedule_hours(self, intervals, hour, remove_at_end=True):
+ """ Schedule hours in intervals. The last matching interval is truncated
+ to match the specified hours.
+
+ It is possible to truncate the last interval at its beginning or ending.
+ However this does nothing on the given interval order that should be
+ submitted accordingly.
+
+ :param list intervals: a list of tuples (beginning datetime, ending datetime)
+ :param int/float hours: number of hours to schedule. It will be converted
+ into a timedelta, but should be submitted as an
+ int or float.
+ :param boolean remove_at_end: remove extra hours at the end of the last
+ matching interval. Otherwise, do it at the
+ beginning.
+
+ :return list results: a list of intervals. If the number of hours to schedule
+ is greater than the possible scheduling in the intervals, no extra-scheduling
+ is done, and results == intervals. """
+ results = []
+ res = datetime.timedelta()
+ limit = datetime.timedelta(hours=hour)
+ for interval in intervals:
+ res += interval[1] - interval[0]
+ if res > limit and remove_at_end:
+ interval = (interval[0], interval[1] + relativedelta(seconds=(limit-res).total_seconds()))
+ elif res > limit:
+ interval = (interval[0] + relativedelta(seconds=(res-limit).total_seconds()), interval[1])
+ results.append(interval)
+ if res > limit:
+ break
return results
- def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
- """Calculates Resource Working Internal Timing Based on Resource Calendar.
+ # --------------------------------------------------
+ # Date and hours computation
+ # --------------------------------------------------
- @param dt_from: start resource schedule calculation.
- @param hours : total number of working hours to be scheduled.
- @param resource: optional resource id, If supplied it will take care of
- resource leave while scheduling.
- @param byday: boolean flag bit enforce day wise scheduling
+ def get_attendances_for_weekdays(self, cr, uid, id, weekdays, context=None):
+ """ Given a list of weekdays, return matching resource.calendar.attendance"""
+ calendar = self.browse(cr, uid, id, context=None)
+ return [att for att in calendar.attendance_ids if int(att.dayofweek) in weekdays]
- @return : list of scheduled working timing based on resource calendar.
+ def get_weekdays(self, cr, uid, id, default_weekdays=None, context=None):
+ """ Return the list of weekdays that contain at least one working interval.
+ If no id is given (no calendar), return default weekdays. """
+ if id is None:
+ return default_weekdays if default_weekdays is not None else [0, 1, 2, 3, 4]
+ calendar = self.browse(cr, uid, id, context=None)
+ weekdays = set()
+ for attendance in calendar.attendance_ids:
+ weekdays.add(int(attendance.dayofweek))
+ return list(weekdays)
+
+ def get_next_day(self, cr, uid, id, day_date, context=None):
+ """ Get following date of day_date, based on resource.calendar. If no
+ calendar is provided, just return the next day.
+
+ :param int id: id of a resource.calendar. If not given, simply add one day
+ to the submitted date.
+ :param date day_date: current day as a date
+
+ :return date: next day of calendar, or just next day """
+ if not id:
+ return day_date + relativedelta(days=1)
+ weekdays = self.get_weekdays(cr, uid, id, context)
+
+ base_index = -1
+ for weekday in weekdays:
+ if weekday > day_date.weekday():
+ break
+ base_index += 1
+
+ new_index = (base_index + 1) % len(weekdays)
+ days = (weekdays[new_index] - day_date.weekday())
+ if days < 0:
+ days = 7 + days
+
+ return day_date + relativedelta(days=days)
+
+ def get_previous_day(self, cr, uid, id, day_date, context=None):
+ """ Get previous date of day_date, based on resource.calendar. If no
+ calendar is provided, just return the previous day.
+
+ :param int id: id of a resource.calendar. If not given, simply remove
+ one day from the submitted date.
+ :param date day_date: current day as a date
+
+ :return date: previous day of calendar, or just previous day """
+ if not id:
+ return day_date + relativedelta(days=-1)
+ weekdays = self.get_weekdays(cr, uid, id, context)
+ weekdays.reverse()
+
+ base_index = -1
+ for weekday in weekdays:
+ if weekday < day_date.weekday():
+ break
+ base_index += 1
+
+ new_index = (base_index + 1) % len(weekdays)
+ days = (weekdays[new_index] - day_date.weekday())
+ if days > 0:
+ days = days - 7
+
+ return day_date + relativedelta(days=days)
+
+ def get_leave_intervals(self, cr, uid, id, resource_id=None,
+ start_datetime=None, end_datetime=None,
+ context=None):
+ """Get the leaves of the calendar. Leaves can be filtered on the resource,
+ the start datetime or the end datetime.
+
+ :param int resource_id: the id of the resource to take into account when
+ computing the leaves. If not set, only general
+ leaves are computed. If set, generic and
+ specific leaves are computed.
+ :param datetime start_datetime: if provided, do not take into account leaves
+ ending before this date.
+ :param datetime end_datetime: if provided, do not take into account leaves
+ beginning after this date.
+
+ :return list leaves: list of tuples (start_datetime, end_datetime) of
+ leave intervals
"""
- res = self.interval_get_multi(cr, uid, [(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)], resource, byday)[(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)]
+ resource_calendar = self.browse(cr, uid, id, context=context)
+ leaves = []
+ for leave in resource_calendar.leave_ids:
+ if leave.resource_id and not resource_id == leave.resource_id.id:
+ continue
+ date_from = datetime.datetime.strptime(leave.date_from, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ if end_datetime and date_from > end_datetime:
+ continue
+ date_to = datetime.datetime.strptime(leave.date_to, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ if start_datetime and date_to < start_datetime:
+ continue
+ leaves.append((date_from, date_to))
+ return leaves
+
+ def get_working_intervals_of_day(self, cr, uid, id, start_dt=None, end_dt=None,
+ leaves=None, compute_leaves=False, resource_id=None,
+ default_interval=None, context=None):
+ """ Get the working intervals of the day based on calendar. This method
+ handle leaves that come directly from the leaves parameter or can be computed.
+
+ :param int id: resource.calendar id; take the first one if is a list
+ :param datetime start_dt: datetime object that is the beginning hours
+ for the working intervals computation; any
+ working interval beginning before start_dt
+ will be truncated. If not set, set to end_dt
+ or today() if no end_dt at 00.00.00.
+ :param datetime end_dt: datetime object that is the ending hour
+ for the working intervals computation; any
+ working interval ending after end_dt
+ will be truncated. If not set, set to start_dt()
+ at 23.59.59.
+ :param list leaves: a list of tuples(start_datetime, end_datetime) that
+ represent leaves.
+ :param boolean compute_leaves: if set and if leaves is None, compute the
+ leaves based on calendar and resource.
+ If leaves is None and compute_leaves false
+ no leaves are taken into account.
+ :param int resource_id: the id of the resource to take into account when
+ computing the leaves. If not set, only general
+ leaves are computed. If set, generic and
+ specific leaves are computed.
+ :param tuple default_interval: if no id, try to return a default working
+ day using default_interval[0] as beginning
+ hour, and default_interval[1] as ending hour.
+ Example: default_interval = (8, 16).
+ Otherwise, a void list of working intervals
+ is returned when id is None.
+
+ :return list intervals: a list of tuples (start_datetime, end_datetime)
+ of work intervals """
+ if isinstance(id, (list, tuple)):
+ id = id[0]
+
+ # Computes start_dt, end_dt (with default values if not set) + off-interval work limits
+ work_limits = []
+ if start_dt is None and end_dt is not None:
+ start_dt = end_dt.replace(hour=0, minute=0, second=0)
+ elif start_dt is None:
+ start_dt = datetime.datetime.now().replace(hour=0, minute=0, second=0)
+ else:
+ work_limits.append((start_dt.replace(hour=0, minute=0, second=0), start_dt))
+ if end_dt is None:
+ end_dt = start_dt.replace(hour=23, minute=59, second=59)
+ else:
+ work_limits.append((end_dt, end_dt.replace(hour=23, minute=59, second=59)))
+ assert start_dt.date() == end_dt.date(), 'get_working_intervals_of_day is restricted to one day'
+
+ intervals = []
+ work_dt = start_dt.replace(hour=0, minute=0, second=0)
+
+ # no calendar: try to use the default_interval, then return directly
+ if id is None:
+ if default_interval:
+ intervals.append((start_dt.replace(hour=default_interval[0]), start_dt.replace(hour=default_interval[1])))
+ return intervals
+
+ working_intervals = []
+ for calendar_working_day in self.get_attendances_for_weekdays(cr, uid, id, [start_dt.weekday()], context):
+ working_interval = (
+ work_dt.replace(hour=int(calendar_working_day.hour_from)),
+ work_dt.replace(hour=int(calendar_working_day.hour_to))
+ )
+ working_intervals += self.interval_remove_leaves(working_interval, work_limits)
+
+ # find leave intervals
+ if leaves is None and compute_leaves:
+ leaves = self.get_leave_intervals(cr, uid, id, resource_id=resource_id, context=None)
+
+ # filter according to leaves
+ for interval in working_intervals:
+ work_intervals = self.interval_remove_leaves(interval, leaves)
+ intervals += work_intervals
+
+ return intervals
+
+ def get_working_hours_of_date(self, cr, uid, id, start_dt=None, end_dt=None,
+ leaves=None, compute_leaves=False, resource_id=None,
+ default_interval=None, context=None):
+ """ Get the working hours of the day based on calendar. This method uses
+ get_working_intervals_of_day to have the work intervals of the day. It
+ then calculates the number of hours contained in those intervals. """
+ res = datetime.timedelta()
+ intervals = self.get_working_intervals_of_day(
+ cr, uid, id,
+ start_dt, end_dt, leaves,
+ compute_leaves, resource_id,
+ default_interval, context)
+ for interval in intervals:
+ res += interval[1] - interval[0]
+ return (res.total_seconds() / 3600.0)
+
+ def get_working_hours(self, cr, uid, id, start_dt, end_dt, compute_leaves=False,
+ resource_id=None, default_interval=None, context=None):
+ hours = 0.0
+ for day in rrule.rrule(rrule.DAILY, dtstart=start_dt,
+ until=end_dt + datetime.timedelta(days=1),
+ byweekday=self.get_weekdays(cr, uid, id, context=context)):
+ hours += self.get_working_hours_of_date(
+ cr, uid, id, start_dt=day,
+ compute_leaves=compute_leaves, resource_id=resource_id,
+ default_interval=default_interval,
+ context=context)
+ return hours
+
+ # --------------------------------------------------
+ # Hours scheduling
+ # --------------------------------------------------
+
+ def _schedule_hours(self, cr, uid, id, hours, day_dt=None,
+ compute_leaves=False, resource_id=None,
+ default_interval=None, context=None):
+ """ Schedule hours of work, using a calendar and an optional resource to
+ compute working and leave days. This method can be used backwards, i.e.
+ scheduling days before a deadline.
+
+ :param int hours: number of hours to schedule. Use a negative number to
+ compute a backwards scheduling.
+ :param datetime day_dt: reference date to compute working days. If days is
+ > 0 date is the starting date. If days is < 0
+ date is the ending date.
+ :param boolean compute_leaves: if set, compute the leaves based on calendar
+ and resource. Otherwise no leaves are taken
+ into account.
+ :param int resource_id: the id of the resource to take into account when
+ computing the leaves. If not set, only general
+ leaves are computed. If set, generic and
+ specific leaves are computed.
+ :param tuple default_interval: if no id, try to return a default working
+ day using default_interval[0] as beginning
+ hour, and default_interval[1] as ending hour.
+ Example: default_interval = (8, 16).
+ Otherwise, a void list of working intervals
+ is returned when id is None.
+
+ :return tuple (datetime, intervals): datetime is the beginning/ending date
+ of the schedulign; intervals are the
+ working intervals of the scheduling.
+
+ Note: Why not using rrule.rrule ? Because rrule does not seem to allow
+ getting back in time.
+ """
+ if day_dt is None:
+ day_dt = datetime.datetime.now()
+ backwards = (hours < 0)
+ hours = abs(hours)
+ intervals = []
+ remaining_hours = hours * 1.0
+ iterations = 0
+ current_datetime = day_dt
+
+ call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, default_interval=default_interval, context=context)
+
+ while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000:
+ if backwards:
+ call_args['end_dt'] = current_datetime
+ else:
+ call_args['start_dt'] = current_datetime
+
+ working_intervals = self.get_working_intervals_of_day(cr, uid, id, **call_args)
+
+ if id is None and not working_interval: # no calendar -> consider working 8 hours
+ remaining_hours -= 8.0
+ elif working_intervals:
+ if backwards:
+ working_intervals.reverse()
+ new_working_intervals = self.interval_schedule_hours(working_intervals, remaining_hours, not backwards)
+ if backwards:
+ new_working_intervals.reverse()
+
+ res = datetime.timedelta()
+ for interval in working_intervals:
+ res += interval[1] - interval[0]
+ remaining_hours -= (res.total_seconds() / 3600.0)
+ if backwards:
+ intervals = new_working_intervals + intervals
+ else:
+ intervals = intervals + new_working_intervals
+ # get next day
+ if backwards:
+ current_datetime = datetime.datetime.combine(self.get_previous_day(cr, uid, id, current_datetime, context), datetime.time(23, 59, 59))
+ else:
+ current_datetime = datetime.datetime.combine(self.get_next_day(cr, uid, id, current_datetime, context), datetime.time())
+ # avoid infinite loops
+ iterations += 1
+
+ return intervals
+
+ def schedule_hours_get_date(self, cr, uid, id, hours, day_dt=None,
+ compute_leaves=False, resource_id=None,
+ default_interval=None, context=None):
+ """ Wrapper on _schedule_hours: return the beginning/ending datetime of
+ an hours scheduling. """
+ res = self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, default_interval, context)
+ return res and res[0][0] or False
+
+ def schedule_hours(self, cr, uid, id, hours, day_dt=None,
+ compute_leaves=False, resource_id=None,
+ default_interval=None, context=None):
+ """ Wrapper on _schedule_hours: return the working intervals of an hours
+ scheduling. """
+ return self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, default_interval, context)
+
+ # --------------------------------------------------
+ # Days scheduling
+ # --------------------------------------------------
+
+ def _schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False,
+ resource_id=None, default_interval=None, context=None):
+ """Schedule days of work, using a calendar and an optional resource to
+ compute working and leave days. This method can be used backwards, i.e.
+ scheduling days before a deadline.
+
+ :param int days: number of days to schedule. Use a negative number to
+ compute a backwards scheduling.
+ :param date day_date: reference date to compute working days. If days is > 0
+ date is the starting date. If days is < 0 date is the
+ ending date.
+ :param boolean compute_leaves: if set, compute the leaves based on calendar
+ and resource. Otherwise no leaves are taken
+ into account.
+ :param int resource_id: the id of the resource to take into account when
+ computing the leaves. If not set, only general
+ leaves are computed. If set, generic and
+ specific leaves are computed.
+ :param tuple default_interval: if no id, try to return a default working
+ day using default_interval[0] as beginning
+ hour, and default_interval[1] as ending hour.
+ Example: default_interval = (8, 16).
+ Otherwise, a void list of working intervals
+ is returned when id is None.
+
+ :return tuple (datetime, intervals): datetime is the beginning/ending date
+ of the schedulign; intervals are the
+ working intervals of the scheduling.
+
+ Implementation note: rrule.rrule is not used because rrule it des not seem
+ to allow getting back in time.
+ """
+ if day_date is None:
+ day_date = datetime.datetime.now()
+ backwards = (days < 0)
+ days = abs(days)
+ intervals = []
+ planned_days = 0
+ iterations = 0
+ if backwards:
+ current_datetime = day_date.replace(hour=23, minute=59, second=59)
+ else:
+ current_datetime = day_date.replace(hour=0, minute=0, second=0)
+
+ while planned_days < days and iterations < 1000:
+ working_intervals = self.get_working_intervals_of_day(
+ cr, uid, id, current_datetime,
+ compute_leaves=compute_leaves, resource_id=resource_id,
+ default_interval=default_interval,
+ context=context)
+ if id is None or working_intervals: # no calendar -> no working hours, but day is considered as worked
+ planned_days += 1
+ intervals += working_intervals
+ # get next day
+ if backwards:
+ current_datetime = self.get_previous_day(cr, uid, id, current_datetime, context)
+ else:
+ current_datetime = self.get_next_day(cr, uid, id, current_datetime, context)
+ # avoid infinite loops
+ iterations += 1
+
+ return intervals
+
+ def schedule_days_get_date(self, cr, uid, id, days, day_date=None, compute_leaves=False,
+ resource_id=None, default_interval=None, context=None):
+ """ Wrapper on _schedule_days: return the beginning/ending datetime of
+ a days scheduling. """
+ res = self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, default_interval, context)
+ return res and res[-1][1] or False
+
+ def schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False,
+ resource_id=None, default_interval=None, context=None):
+ """ Wrapper on _schedule_days: return the working intervals of a days
+ scheduling. """
+ return self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, default_interval, context)
+
+ # --------------------------------------------------
+ # Compatibility / to clean / to remove
+ # --------------------------------------------------
+
+ def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
+ """ Used in hr_payroll/hr_payroll.py
+
+ :deprecated: OpenERP saas-3. Use get_working_hours_of_date instead. Note:
+ since saas-3, take hour/minutes into account, not just the whole day."""
+ if isinstance(day, datetime.datetime):
+ day = day.replace(hour=0, minute=0)
+ return self.get_working_hours_of_date(cr, uid, resource_calendar_id.id, start_dt=day, context=None)
+
+ def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
+ """ Schedule hours backwards. Used in mrp_operations/mrp_operations.py.
+
+ :deprecated: OpenERP saas-3. Use schedule_hours instead. Note: since
+ saas-3, counts leave hours instead of all-day leaves."""
+ return self.schedule_hours(
+ cr, uid, id, hours * -1.0,
+ day_dt=dt_from.replace(minute=0, second=0),
+ compute_leaves=True, resource_id=resource,
+ default_interval=(8, 16)
+ )
+
+ def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
+ """ Used in mrp_operations/mrp_operations.py (default parameters) and in
+ interval_get()
+
+ :deprecated: OpenERP saas-3. Use schedule_hours instead. Note:
+ Byday was not used. Since saas-3, counts Leave hours instead of all-day leaves."""
+ res = {}
+ for dt_str, hours, calendar_id in date_and_hours_by_cal:
+ result = self.schedule_hours(
+ cr, uid, calendar_id, hours,
+ day_dt=datetime.datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S').replace(minute=0, second=0),
+ compute_leaves=True, resource_id=resource,
+ default_interval=(8, 16)
+ )
+ res[(dt_str, hours, calendar_id)] = result
+ return res
+
+ def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
+ """ Unifier of interval_get_multi. Used in: mrp_operations/mrp_operations.py,
+ crm/crm_lead.py (res given).
+
+ :deprecated: OpenERP saas-3. Use get_working_hours instead."""
+ res = self.interval_get_multi(
+ cr, uid, [(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)], resource, byday)[(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)]
return res
def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
- """ Calculates the Total Working hours based on given start_date to
- end_date, If resource id is supplied that it will consider the source
- leaves also in calculating the hours.
+ """ Unused wrapper.
- @param dt_from : date start to calculate hours
- @param dt_end : date end to calculate hours
- @param resource: optional resource id, If given resource leave will be
- considered.
-
- @return : Total number of working hours based dt_from and dt_end and
- resource if supplied.
- """
+ :deprecated: OpenERP saas-3. Use get_working_hours instead."""
return self._interval_hours_get(cr, uid, id, dt_from, dt_to, resource_id=resource)
def _interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource_id=False, timezone_from_uid=None, exclude_leaves=True, context=None):
- """ Calculates the Total Working hours based on given start_date to
- end_date, If resource id is supplied that it will consider the source
- leaves also in calculating the hours.
+ """ Computes working hours between two dates, taking always same hour/minuts.
- @param dt_from : date start to calculate hours
- @param dt_end : date end to calculate hours
- @param resource_id: optional resource id, If given resource leave will be
- considered.
- @param timezone_from_uid: optional uid, if given we will considerer
- working hours in that user timezone
- @param exclude_leaves: optionnal, if set to True (default) we will exclude
- resource leaves from working hours
- @param context: current request context
- @return : Total number of working hours based dt_from and dt_end and
- resource if supplied.
- """
- utc_tz = pytz.timezone('UTC')
- local_tz = utc_tz
-
- if timezone_from_uid:
- users_obj = self.pool.get('res.users')
- user_timezone = users_obj.browse(cr, uid, timezone_from_uid, context=context).partner_id.tz
- if user_timezone:
- try:
- local_tz = pytz.timezone(user_timezone)
- except pytz.UnknownTimeZoneError:
- pass # fallback to UTC as local timezone
-
- def utc_to_local_zone(naive_datetime):
- utc_dt = utc_tz.localize(naive_datetime, is_dst=False)
- return utc_dt.astimezone(local_tz)
-
- def float_time_convert(float_val):
- factor = float_val < 0 and -1 or 1
- val = abs(float_val)
- return (factor * int(math.floor(val)), int(round((val % 1) * 60)))
-
- # Get slots hours per day
- # {day_of_week: [(8, 12), (13, 17), ...], ...}
- hours_range_per_weekday = {}
- if id:
- cr.execute("select dayofweek, hour_from,hour_to from resource_calendar_attendance where calendar_id=%s order by hour_from", (id,))
- for weekday, hour_from, hour_to in cr.fetchall():
- weekday = int(weekday)
- hours_range_per_weekday.setdefault(weekday, [])
- hours_range_per_weekday[weekday].append((hour_from, hour_to))
- else:
- # considering default working hours (Monday -> Friday, 8 -> 12, 13 -> 17)
- for weekday in range(5):
- hours_range_per_weekday[weekday] = [(8, 12), (13, 17)]
-
- ## Interval between dt_from - dt_to
- ##
- ## dt_from dt_to
- ## =============|==================|============
- ## [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]
- ##
- ## [ : start of range
- ## ] : end of range
- ##
- ## case 1: range end before interval start (skip)
- ## case 2: range overlap interval start (fit start to internal)
- ## case 3: range within interval
- ## case 4: range overlap interval end (fit end to interval)
- ## case 5: range start after interval end (skip)
-
- interval_start = utc_to_local_zone(dt_from)
- interval_end = utc_to_local_zone(dt_to)
- hours_timedelta = timedelta()
-
- # Get leaves for requested resource
- dt_leaves = set([])
- if exclude_leaves and id:
- dt_leaves = set(self._get_leaves(cr, uid, id, resource=resource_id))
-
- for day in rrule.rrule(rrule.DAILY, dtstart=interval_start,
- until=interval_end+timedelta(days=1),
- byweekday=hours_range_per_weekday.keys()):
- if exclude_leaves and day.strftime('%Y-%m-%d') in dt_leaves:
- # XXX: futher improve leave management to allow for partial day leave
- continue
- for (range_from, range_to) in hours_range_per_weekday.get(day.weekday(), []):
- range_from_hour, range_from_min = float_time_convert(range_from)
- range_to_hour, range_to_min = float_time_convert(range_to)
- daytime_start = local_tz.localize(day.replace(hour=range_from_hour, minute=range_from_min, second=0, tzinfo=None))
- daytime_end = local_tz.localize(day.replace(hour=range_to_hour, minute=range_to_min, second=0, tzinfo=None))
-
- # case 1 & 5: time range out of interval
- if daytime_end < interval_start or daytime_start > interval_end:
- continue
- # case 2 & 4: adjust start, end to fit within interval
- daytime_start = max(daytime_start, interval_start)
- daytime_end = min(daytime_end, interval_end)
-
- # case 2+, 4+, 3
- hours_timedelta += (daytime_end - daytime_start)
-
- # return timedelta converted to hours
- return (hours_timedelta.days * 24.0 + hours_timedelta.seconds / 3600.0)
+ :deprecated: OpenERP saas-3. Use get_working_hours instead. Note: since saas-3,
+ now resets hour/minuts. Now counts leave hours instead of all-day leaves."""
+ return self.get_working_hours(
+ cr, uid, id, dt_from, dt_to,
+ compute_leaves=(not exclude_leaves), resource_id=resource_id,
+ default_interval=(8, 16), context=context)
class resource_calendar_attendance(osv.osv):
@@ -374,6 +683,8 @@ class resource_resource(osv.osv):
def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
"""
Return a list of Resource Class objects for the resources allocated to the phase.
+
+ NOTE: Used in project/project.py
"""
resource_objs = {}
user_pool = self.pool.get('res.users')
@@ -401,6 +712,8 @@ class resource_resource(osv.osv):
@param calendar_id : working calendar of the project
@param resource_id : resource working on phase/task
@param resource_calendar : working calendar of the resource
+
+ NOTE: used in project/project.py, and in generate_resources
"""
resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
leave_list = []
@@ -415,10 +728,10 @@ class resource_resource(osv.osv):
], context=context)
leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
for i in range(len(leaves)):
- dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
- dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
+ dt_start = datetime.datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
+ dt_end = datetime.datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
no = dt_end - dt_start
- [leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
+ [leave_list.append((dt_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
leave_list.sort()
return leave_list
@@ -426,6 +739,8 @@ class resource_resource(osv.osv):
"""
Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
@param calendar_id : working calendar of the project
+
+ NOTE: used in project/project.py
"""
if not calendar_id:
# Calendar is not specified: working days: 24/7
diff --git a/addons/resource/resource_view.xml b/addons/resource/resource_view.xml
index 8954adae80a..74f71ecfeea 100644
--- a/addons/resource/resource_view.xml
+++ b/addons/resource/resource_view.xml
@@ -60,12 +60,13 @@
@@ -245,13 +246,17 @@
+
Resource Leavesresource.calendar.leavestree,form,calendar
+
-
+
+
+
diff --git a/addons/resource/test/resource.yml b/addons/resource/test/resource.yml
index 28604518b7b..b085c0734c4 100644
--- a/addons/resource/test/resource.yml
+++ b/addons/resource/test/resource.yml
@@ -26,7 +26,7 @@
dt = now - timedelta(days=now.weekday())
for resource in resources:
result = calendar_pool.working_hours_on_day(cr, uid, resource.calendar_id, dt, context)
- assert result == 9.0, 'Wrong calculation of day work hour availability of the Resource.'
+ assert result == 9.0, 'Wrong calculation of day work hour availability of the Resource (found %d).' % result
-
Now, resource "Developer" drafted leave on Thursday in this week.
-
diff --git a/addons/resource/tests/__init__.py b/addons/resource/tests/__init__.py
new file mode 100644
index 00000000000..f22158c04b8
--- /dev/null
+++ b/addons/resource/tests/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2013-TODAY OpenERP S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from openerp.addons.resource.tests import test_resource
+
+checks = [
+ test_resource,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/resource/tests/common.py b/addons/resource/tests/common.py
new file mode 100644
index 00000000000..74b91abde2e
--- /dev/null
+++ b/addons/resource/tests/common.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2013-TODAY OpenERP S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from datetime import datetime
+
+from openerp.tests import common
+
+
+class TestResourceCommon(common.TransactionCase):
+
+ def setUp(self):
+ super(TestResourceCommon, self).setUp()
+ cr, uid = self.cr, self.uid
+ if not hasattr(self, 'context'):
+ self.context = {}
+
+ # Usefull models
+ self.resource_resource = self.registry('resource.resource')
+ self.resource_calendar = self.registry('resource.calendar')
+ self.resource_attendance = self.registry('resource.calendar.attendance')
+ self.resource_leaves = self.registry('resource.calendar.leaves')
+
+ # Some demo data
+ self.date1 = datetime.strptime('2013-02-12 09:08:07', '%Y-%m-%d %H:%M:%S') # weekday() returns 1, isoweekday() returns 2
+ self.date2 = datetime.strptime('2013-02-15 10:11:12', '%Y-%m-%d %H:%M:%S') # weekday() returns 4, isoweekday() returns 5
+ # Leave1: 19/02/2013, from 9 to 12, is a day 1
+ self.leave1_start = datetime.strptime('2013-02-19 09:00:00', '%Y-%m-%d %H:%M:%S')
+ self.leave1_end = datetime.strptime('2013-02-19 12:00:00', '%Y-%m-%d %H:%M:%S')
+ # Leave2: 22/02/2013, from 9 to 15, is a day 4
+ self.leave2_start = datetime.strptime('2013-02-22 09:00:00', '%Y-%m-%d %H:%M:%S')
+ self.leave2_end = datetime.strptime('2013-02-22 15:00:00', '%Y-%m-%d %H:%M:%S')
+ # Leave3: 25/02/2013 (day0) -> 01/03/2013 (day4)
+ self.leave3_start = datetime.strptime('2013-02-25 13:00:00', '%Y-%m-%d %H:%M:%S')
+ self.leave3_end = datetime.strptime('2013-03-01 11:30:00', '%Y-%m-%d %H:%M:%S')
+
+ # Resource data
+ # Calendar working days: 1 (8-16 -> 8hours), 4 (8-13, 16-23 -> 12hours)
+ self.calendar_id = self.resource_calendar.create(
+ cr, uid, {
+ 'name': 'TestCalendar',
+ }
+ )
+ self.att1_id = self.resource_attendance.create(
+ cr, uid, {
+ 'name': 'Att1',
+ 'dayofweek': '1',
+ 'hour_from': 8,
+ 'hour_to': 16,
+ 'calendar_id': self.calendar_id,
+ }
+ )
+ self.att2_id = self.resource_attendance.create(
+ cr, uid, {
+ 'name': 'Att2',
+ 'dayofweek': '4',
+ 'hour_from': 8,
+ 'hour_to': 13,
+ 'calendar_id': self.calendar_id,
+ }
+ )
+ self.att3_id = self.resource_attendance.create(
+ cr, uid, {
+ 'name': 'Att3',
+ 'dayofweek': '4',
+ 'hour_from': 16,
+ 'hour_to': 23,
+ 'calendar_id': self.calendar_id,
+ }
+ )
+ self.resource1_id = self.resource_resource.create(
+ cr, uid, {
+ 'name': 'TestResource1',
+ 'resource_type': 'user',
+ 'time_efficiency': 150.0,
+ 'calendar_id': self.calendar_id,
+ }
+ )
+ self.leave1_id = self.resource_leaves.create(
+ cr, uid, {
+ 'name': 'GenericLeave',
+ 'calendar_id': self.calendar_id,
+ 'date_from': self.leave1_start,
+ 'date_to': self.leave1_end,
+ }
+ )
+ self.leave2_id = self.resource_leaves.create(
+ cr, uid, {
+ 'name': 'ResourceLeave',
+ 'calendar_id': self.calendar_id,
+ 'resource_id': self.resource1_id,
+ 'date_from': self.leave2_start,
+ 'date_to': self.leave2_end,
+ }
+ )
+ self.leave3_id = self.resource_leaves.create(
+ cr, uid, {
+ 'name': 'ResourceLeave2',
+ 'calendar_id': self.calendar_id,
+ 'resource_id': self.resource1_id,
+ 'date_from': self.leave3_start,
+ 'date_to': self.leave3_end,
+ }
+ )
+ # Some browse data
+ self.calendar = self.resource_calendar.browse(cr, uid, self.calendar_id)
diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py
new file mode 100644
index 00000000000..346317449d3
--- /dev/null
+++ b/addons/resource/tests/test_resource.py
@@ -0,0 +1,445 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2013-TODAY OpenERP S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from datetime import datetime, timedelta
+from dateutil.relativedelta import relativedelta
+
+from openerp.addons.resource.tests.common import TestResourceCommon
+
+
+class TestResource(TestResourceCommon):
+
+ def test_00_intervals(self):
+ intervals = [
+ (
+ datetime.strptime('2013-02-04 09:00:00', '%Y-%m-%d %H:%M:%S'),
+ datetime.strptime('2013-02-04 11:00:00', '%Y-%m-%d %H:%M:%S')
+ ), (
+ datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'),
+ datetime.strptime('2013-02-04 12:00:00', '%Y-%m-%d %H:%M:%S')
+ ), (
+ datetime.strptime('2013-02-04 11:00:00', '%Y-%m-%d %H:%M:%S'),
+ datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S')
+ ), (
+ datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'),
+ datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S')
+ ), (
+ datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'),
+ datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S')
+ ), (
+ datetime.strptime('2013-02-04 18:00:00', '%Y-%m-%d %H:%M:%S'),
+ datetime.strptime('2013-02-04 19:00:00', '%Y-%m-%d %H:%M:%S')
+ )
+ ]
+
+ # Test: interval cleaning
+ cleaned_intervals = self.resource_calendar.interval_clean(intervals)
+ self.assertEqual(len(cleaned_intervals), 3, 'resource_calendar: wrong interval cleaning')
+ # First interval: 03, unchanged
+ self.assertEqual(cleaned_intervals[0][0], datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning')
+ self.assertEqual(cleaned_intervals[0][1], datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning')
+ # Second intreval: 04, 08-14, combining 08-12 and 11-14, 09-11 being inside 08-12
+ self.assertEqual(cleaned_intervals[1][0], datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning')
+ self.assertEqual(cleaned_intervals[1][1], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning')
+ # Third interval: 04, 17-21, 18-19 being inside 17-21
+ self.assertEqual(cleaned_intervals[2][0], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning')
+ self.assertEqual(cleaned_intervals[2][1], datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning')
+
+ # Test: disjoint removal
+ working_interval = (datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), datetime.strptime('2013-02-04 18:00:00', '%Y-%m-%d %H:%M:%S'))
+ result = self.resource_calendar.interval_remove_leaves(working_interval, intervals)
+ self.assertEqual(len(result), 1, 'resource_calendar: wrong leave removal from interval')
+ # First interval: 04, 14-17
+ self.assertEqual(result[0][0], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ self.assertEqual(result[0][1], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+
+ # Test: schedule hours on intervals
+ result = self.resource_calendar.interval_schedule_hours(cleaned_intervals, 5.5)
+ self.assertEqual(len(result), 2, 'resource_calendar: wrong hours scheduling in interval')
+ # First interval: 03, 8-10 untouches
+ self.assertEqual(result[0][0], datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ self.assertEqual(result[0][1], datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ # First interval: 04, 08-11:30
+ self.assertEqual(result[1][0], datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ self.assertEqual(result[1][1], datetime.strptime('2013-02-04 11:30:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+
+ # Test: schedule hours on intervals, backwards
+ cleaned_intervals.reverse()
+ result = self.resource_calendar.interval_schedule_hours(cleaned_intervals, 5.5, remove_at_end=False)
+ self.assertEqual(len(result), 2, 'resource_calendar: wrong hours scheduling in interval')
+ # First interval: 03, 8-10 untouches
+ self.assertEqual(result[0][0], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ self.assertEqual(result[0][1], datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ # First interval: 04, 08-11:30
+ self.assertEqual(result[1][0], datetime.strptime('2013-02-04 12:30:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+ self.assertEqual(result[1][1], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval')
+
+ def test_10_calendar_basics(self):
+ """ Testing basic method of resource.calendar """
+ cr, uid = self.cr, self.uid
+
+ # --------------------------------------------------
+ # Test1: get_next_day
+ # --------------------------------------------------
+
+ # Test: next day: next day after day1 is day4
+ date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date1.date())
+ self.assertEqual(date, self.date2.date(), 'resource_calendar: wrong next day computing')
+
+ # Test: next day: next day after day4 is (day1+7)
+ date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date2.date())
+ self.assertEqual(date, self.date1.date() + relativedelta(days=7), 'resource_calendar: wrong next day computing')
+
+ # Test: next day: next day after day4+1 is (day1+7)
+ date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date2.date() + relativedelta(days=1))
+ self.assertEqual(date, self.date1.date() + relativedelta(days=7), 'resource_calendar: wrong next day computing')
+
+ # Test: next day: next day after day1-1 is day1
+ date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date1.date() + relativedelta(days=-1))
+ self.assertEqual(date, self.date1.date(), 'resource_calendar: wrong next day computing')
+
+ # --------------------------------------------------
+ # Test2: get_previous_day
+ # --------------------------------------------------
+
+ # Test: previous day: previous day before day1 is (day4-7)
+ date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date1.date())
+ self.assertEqual(date, self.date2.date() + relativedelta(days=-7), 'resource_calendar: wrong previous day computing')
+
+ # Test: previous day: previous day before day4 is day1
+ date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date2.date())
+ self.assertEqual(date, self.date1.date(), 'resource_calendar: wrong previous day computing')
+
+ # Test: previous day: previous day before day4+1 is day4
+ date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date2.date() + relativedelta(days=1))
+ self.assertEqual(date, self.date2.date(), 'resource_calendar: wrong previous day computing')
+
+ # Test: previous day: previous day before day1-1 is (day4-7)
+ date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date1.date() + relativedelta(days=-1))
+ self.assertEqual(date, self.date2.date() + relativedelta(days=-7), 'resource_calendar: wrong previous day computing')
+
+ # --------------------------------------------------
+ # Test3: misc
+ # --------------------------------------------------
+
+ weekdays = self.resource_calendar.get_weekdays(cr, uid, self.calendar_id)
+ self.assertEqual(weekdays, [1, 4], 'resource_calendar: wrong weekdays computing')
+
+ attendances = self.resource_calendar.get_attendances_for_weekdays(cr, uid, self.calendar_id, [2, 3, 4, 5])
+ self.assertEqual(set([att.id for att in attendances]), set([self.att2_id, self.att3_id]),
+ 'resource_calendar: wrong attendances filtering by weekdays computing')
+
+ def test_20_calendar_working_intervals(self):
+ """ Testing working intervals computing method of resource.calendar """
+ cr, uid = self.cr, self.uid
+ _format = '%Y-%m-%d %H:%M:%S'
+
+ # Test: day0 without leaves: 1 interval
+ intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date1)
+ self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 09:08:07', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong working intervals')
+
+ # Test: day3 without leaves: 2 interval
+ intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date2)
+ self.assertEqual(len(intervals), 2, 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-15 10:11:12', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-15 13:00:00', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[1][0], datetime.strptime('2013-02-15 16:00:00', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[1][1], datetime.strptime('2013-02-15 23:00:00', _format), 'resource_calendar: wrong working intervals')
+
+ # Test: day0 with leaves outside range: 1 interval
+ intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date1.replace(hour=0), compute_leaves=True)
+ self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 08:00:00', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong working intervals')
+
+ # Test: day0 with leaves: 2 intervals because of leave between 9 ans 12, ending at 15:45:30
+ intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id,
+ start_dt=self.date1.replace(hour=8) + relativedelta(days=7),
+ end_dt=self.date1.replace(hour=15, minute=45, second=30) + relativedelta(days=7),
+ compute_leaves=True)
+ self.assertEqual(len(intervals), 2, 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:08:07', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 09:00:00', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[1][0], datetime.strptime('2013-02-19 12:00:00', _format), 'resource_calendar: wrong working intervals')
+ self.assertEqual(intervals[1][1], datetime.strptime('2013-02-19 15:45:30', _format), 'resource_calendar: wrong working intervals')
+
+ def test_30_calendar_working_days(self):
+ """ Testing calendar hours computation on a working day """
+ cr, uid = self.cr, self.uid
+ _format = '%Y-%m-%d %H:%M:%S'
+
+ # Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00
+ intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date1.replace(hour=10, minute=30, second=0))
+ self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 10:30:00', _format), 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+ # Test: hour computation for same interval, should give 5.5
+ wh = self.resource_calendar.get_working_hours_of_date(cr, uid, self.calendar_id, start_dt=self.date1.replace(hour=10, minute=30, second=0))
+ self.assertEqual(wh, 5.5, 'resource_calendar: wrong working interval / day time computing')
+
+ # Test: day1+7 on leave, without leave computation
+ intervals = self.resource_calendar.get_working_intervals_of_day(
+ cr, uid, self.calendar_id,
+ start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=7)
+ )
+ # Result: day1 (08->16)
+ self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working interval/day computing')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+
+ # Test: day1+7 on leave, with generic leave computation
+ intervals = self.resource_calendar.get_working_intervals_of_day(
+ cr, uid, self.calendar_id,
+ start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=7),
+ compute_leaves=True
+ )
+ # Result: day1 (08->09 + 12->16)
+ self.assertEqual(len(intervals), 2, 'resource_calendar: wrong working interval/day computing')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 09:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[1][0], datetime.strptime('2013-02-19 12:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[1][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+
+ # Test: day1+14 on leave, with generic leave computation
+ intervals = self.resource_calendar.get_working_intervals_of_day(
+ cr, uid, self.calendar_id,
+ start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=14),
+ compute_leaves=True
+ )
+ # Result: day1 (08->16)
+ self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working interval/day computing')
+ self.assertEqual(intervals[0][0], datetime.strptime('2013-02-26 08:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+ self.assertEqual(intervals[0][1], datetime.strptime('2013-02-26 16:00:00', _format), 'resource_calendar: wrong working interval / day computing')
+
+ # Test: day1+14 on leave, with resource leave computation
+ intervals = self.resource_calendar.get_working_intervals_of_day(
+ cr, uid, self.calendar_id,
+ start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=14),
+ compute_leaves=True,
+ resource_id=self.resource1_id
+ )
+ # Result: nothing, because on leave
+ self.assertEqual(len(intervals), 0, 'resource_calendar: wrong working interval/day computing')
+
+ def test_40_calendar_hours_scheduling(self):
+ """ Testing calendar hours scheduling """
+ cr, uid = self.cr, self.uid
+ _format = '%Y-%m-%d %H:%M:%S'
+
+ # --------------------------------------------------
+ # Test0: schedule hours backwards (old interval_min_get)
+ # Done without calendar
+ # --------------------------------------------------
+
+ # Done without calendar
+ # res = self.resource_calendar.interval_min_get(cr, uid, None, self.date1, 40, resource=False)
+ # res: (datetime.datetime(2013, 2, 7, 9, 8, 7), datetime.datetime(2013, 2, 12, 9, 8, 7))
+
+ # --------------------------------------------------
+ # Test1: schedule hours backwards (old interval_min_get)
+ # --------------------------------------------------
+
+ # res = self.resource_calendar.interval_min_get(cr, uid, self.calendar_id, self.date1, 40, resource=False)
+ # (datetime.datetime(2013, 1, 29, 9, 0), datetime.datetime(2013, 1, 29, 16, 0))
+ # (datetime.datetime(2013, 2, 1, 8, 0), datetime.datetime(2013, 2, 1, 13, 0))
+ # (datetime.datetime(2013, 2, 1, 16, 0), datetime.datetime(2013, 2, 1, 23, 0))
+ # (datetime.datetime(2013, 2, 5, 8, 0), datetime.datetime(2013, 2, 5, 16, 0))
+ # (datetime.datetime(2013, 2, 8, 8, 0), datetime.datetime(2013, 2, 8, 13, 0))
+ # (datetime.datetime(2013, 2, 8, 16, 0), datetime.datetime(2013, 2, 8, 23, 0))
+ # (datetime.datetime(2013, 2, 12, 8, 0), datetime.datetime(2013, 2, 12, 9, 0))
+
+ res = self.resource_calendar.schedule_hours(cr, uid, self.calendar_id, -40, day_dt=self.date1.replace(minute=0, second=0))
+ # current day, limited at 09:00 because of day_dt specified -> 1 hour
+ self.assertEqual(res[-1][0], datetime.strptime('2013-02-12 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-1][1], datetime.strptime('2013-02-12 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ # previous days: 5+7 hours / 8 hours / 5+7 hours -> 32 hours
+ self.assertEqual(res[-2][0], datetime.strptime('2013-02-08 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-2][1], datetime.strptime('2013-02-08 23:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-3][0], datetime.strptime('2013-02-08 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-3][1], datetime.strptime('2013-02-08 13:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-4][0], datetime.strptime('2013-02-05 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-4][1], datetime.strptime('2013-02-05 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-5][0], datetime.strptime('2013-02-01 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-5][1], datetime.strptime('2013-02-01 23:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-6][0], datetime.strptime('2013-02-01 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-6][1], datetime.strptime('2013-02-01 13:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ # 7 hours remaining
+ self.assertEqual(res[-7][0], datetime.strptime('2013-01-29 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[-7][1], datetime.strptime('2013-01-29 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ # Compute scheduled hours
+ td = timedelta()
+ for item in res:
+ td += item[1] - item[0]
+ self.assertEqual(td.total_seconds() / 3600.0, 40.0, 'resource_calendar: wrong hours scheduling')
+
+ # --------------------------------------------------
+ # Test2: schedule hours forward (old interval_get)
+ # --------------------------------------------------
+
+ # res = self.resource_calendar.interval_get(cr, uid, self.calendar_id, self.date1, 40, resource=False, byday=True)
+ # (datetime.datetime(2013, 2, 12, 9, 0), datetime.datetime(2013, 2, 12, 16, 0))
+ # (datetime.datetime(2013, 2, 15, 8, 0), datetime.datetime(2013, 2, 15, 13, 0))
+ # (datetime.datetime(2013, 2, 15, 16, 0), datetime.datetime(2013, 2, 15, 23, 0))
+ # (datetime.datetime(2013, 2, 22, 8, 0), datetime.datetime(2013, 2, 22, 13, 0))
+ # (datetime.datetime(2013, 2, 22, 16, 0), datetime.datetime(2013, 2, 22, 23, 0))
+ # (datetime.datetime(2013, 2, 26, 8, 0), datetime.datetime(2013, 2, 26, 16, 0))
+ # (datetime.datetime(2013, 3, 1, 8, 0), datetime.datetime(2013, 3, 1, 9, 0))
+
+ res = self.resource_calendar.schedule_hours(
+ cr, uid, self.calendar_id, 40,
+ day_dt=self.date1.replace(minute=0, second=0)
+ )
+ self.assertEqual(res[0][0], datetime.strptime('2013-02-12 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[1][0], datetime.strptime('2013-02-15 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[1][1], datetime.strptime('2013-02-15 13:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[2][0], datetime.strptime('2013-02-15 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[2][1], datetime.strptime('2013-02-15 23:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[3][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[3][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[4][0], datetime.strptime('2013-02-22 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[4][1], datetime.strptime('2013-02-22 13:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[5][0], datetime.strptime('2013-02-22 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[5][1], datetime.strptime('2013-02-22 23:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[6][0], datetime.strptime('2013-02-26 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[6][1], datetime.strptime('2013-02-26 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ td = timedelta()
+ for item in res:
+ td += item[1] - item[0]
+ self.assertEqual(td.total_seconds() / 3600.0, 40.0, 'resource_calendar: wrong hours scheduling')
+
+ # res = self.resource_calendar.interval_get(cr, uid, self.calendar_id, self.date1, 40, resource=self.resource1_id, byday=True)
+ # (datetime.datetime(2013, 2, 12, 9, 0), datetime.datetime(2013, 2, 12, 16, 0))
+ # (datetime.datetime(2013, 2, 15, 8, 0), datetime.datetime(2013, 2, 15, 13, 0))
+ # (datetime.datetime(2013, 2, 15, 16, 0), datetime.datetime(2013, 2, 15, 23, 0))
+ # (datetime.datetime(2013, 3, 1, 8, 0), datetime.datetime(2013, 3, 1, 13, 0))
+ # (datetime.datetime(2013, 3, 1, 16, 0), datetime.datetime(2013, 3, 1, 23, 0))
+ # (datetime.datetime(2013, 3, 5, 8, 0), datetime.datetime(2013, 3, 5, 16, 0))
+ # (datetime.datetime(2013, 3, 8, 8, 0), datetime.datetime(2013, 3, 8, 9, 0))
+
+ res = self.resource_calendar.schedule_hours(
+ cr, uid, self.calendar_id, 40,
+ day_dt=self.date1.replace(minute=0, second=0),
+ compute_leaves=True,
+ resource_id=self.resource1_id
+ )
+ self.assertEqual(res[0][0], datetime.strptime('2013-02-12 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[1][0], datetime.strptime('2013-02-15 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[1][1], datetime.strptime('2013-02-15 13:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[2][0], datetime.strptime('2013-02-15 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[2][1], datetime.strptime('2013-02-15 23:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[3][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[3][1], datetime.strptime('2013-02-19 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[4][0], datetime.strptime('2013-02-19 12:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[4][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[5][0], datetime.strptime('2013-02-22 08:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[5][1], datetime.strptime('2013-02-22 09:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[6][0], datetime.strptime('2013-02-22 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[6][1], datetime.strptime('2013-02-22 23:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[7][0], datetime.strptime('2013-03-01 11:30:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[7][1], datetime.strptime('2013-03-01 13:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[8][0], datetime.strptime('2013-03-01 16:00:00', _format), 'resource_calendar: wrong hours scheduling')
+ self.assertEqual(res[8][1], datetime.strptime('2013-03-01 22:30:00', _format), 'resource_calendar: wrong hours scheduling')
+ td = timedelta()
+ for item in res:
+ td += item[1] - item[0]
+ self.assertEqual(td.total_seconds() / 3600.0, 40.0, 'resource_calendar: wrong hours scheduling')
+
+ # --------------------------------------------------
+ # Test3: working hours (old _interval_hours_get)
+ # --------------------------------------------------
+
+ # old API: resource without leaves
+ # res: 2 weeks -> 40 hours
+ res = self.resource_calendar._interval_hours_get(
+ cr, uid, self.calendar_id,
+ self.date1.replace(hour=6, minute=0),
+ self.date2.replace(hour=23, minute=0) + relativedelta(days=7),
+ resource_id=self.resource1_id, exclude_leaves=True)
+ self.assertEqual(res, 40.0, 'resource_calendar: wrong _interval_hours_get compatibility computation')
+
+ # new API: resource without leaves
+ # res: 2 weeks -> 40 hours
+ res = self.resource_calendar.get_working_hours(
+ cr, uid, self.calendar_id,
+ self.date1.replace(hour=6, minute=0),
+ self.date2.replace(hour=23, minute=0) + relativedelta(days=7),
+ compute_leaves=False, resource_id=self.resource1_id)
+ self.assertEqual(res, 40.0, 'resource_calendar: wrong get_working_hours computation')
+
+ # old API: resource and leaves
+ # res: 2 weeks -> 40 hours - (3+4) leave hours
+ res = self.resource_calendar._interval_hours_get(
+ cr, uid, self.calendar_id,
+ self.date1.replace(hour=6, minute=0),
+ self.date2.replace(hour=23, minute=0) + relativedelta(days=7),
+ resource_id=self.resource1_id, exclude_leaves=False)
+ self.assertEqual(res, 33.0, 'resource_calendar: wrong _interval_hours_get compatibility computation')
+
+ # new API: resource and leaves
+ # res: 2 weeks -> 40 hours - (3+4) leave hours
+ res = self.resource_calendar.get_working_hours(
+ cr, uid, self.calendar_id,
+ self.date1.replace(hour=6, minute=0),
+ self.date2.replace(hour=23, minute=0) + relativedelta(days=7),
+ compute_leaves=True, resource_id=self.resource1_id)
+ self.assertEqual(res, 33.0, 'resource_calendar: wrong get_working_hours computation')
+
+ # --------------------------------------------------
+ # Test4: misc
+ # --------------------------------------------------
+
+ # Test without calendar and default_interval
+ res = self.resource_calendar.get_working_hours(
+ cr, uid, None,
+ self.date1.replace(hour=6, minute=0),
+ self.date2.replace(hour=23, minute=0),
+ compute_leaves=True, resource_id=self.resource1_id,
+ default_interval=(8, 16))
+ self.assertEqual(res, 32.0, 'resource_calendar: wrong get_working_hours computation')
+
+ def test_50_calendar_schedule_days(self):
+ """ Testing calendar days scheduling """
+ cr, uid = self.cr, self.uid
+ _format = '%Y-%m-%d %H:%M:%S'
+
+ # --------------------------------------------------
+ # Test1: with calendar
+ # --------------------------------------------------
+
+ res = self.resource_calendar.schedule_days_get_date(cr, uid, self.calendar_id, 5, day_date=self.date1)
+ self.assertEqual(res.date(), datetime.strptime('2013-02-26 00:0:00', _format).date(), 'resource_calendar: wrong days scheduling')
+
+ res = self.resource_calendar.schedule_days_get_date(
+ cr, uid, self.calendar_id, 5, day_date=self.date1,
+ compute_leaves=True, resource_id=self.resource1_id)
+ self.assertEqual(res.date(), datetime.strptime('2013-03-01 00:0:00', _format).date(), 'resource_calendar: wrong days scheduling')
+
+ # --------------------------------------------------
+ # Test2: misc
+ # --------------------------------------------------
+
+ # Without calendar, should only count days -> 12 -> 16, 5 days with default intervals
+ res = self.resource_calendar.schedule_days_get_date(cr, uid, None, 5, day_date=self.date1, default_interval=(8, 16))
+ self.assertEqual(res, datetime.strptime('2013-02-16 16:00:00', _format), 'resource_calendar: wrong days scheduling')