mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 00:52:07 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
301
odoo-bringout-oca-ocb-base/odoo/tools/date_utils.py
Normal file
301
odoo-bringout-oca-ocb-base/odoo/tools/date_utils.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import math
|
||||
import calendar
|
||||
from datetime import date, datetime, time
|
||||
from typing import Tuple
|
||||
|
||||
import babel
|
||||
import pytz
|
||||
from dateutil.relativedelta import relativedelta, weekdays
|
||||
|
||||
from .func import lazy
|
||||
from odoo.loglevels import ustr
|
||||
|
||||
|
||||
def date_type(value):
|
||||
''' Return either the datetime.datetime class or datetime.date type whether `value` is a datetime or a date.
|
||||
|
||||
:param value: A datetime.datetime or datetime.date object.
|
||||
:return: datetime.datetime or datetime.date
|
||||
'''
|
||||
return datetime if isinstance(value, datetime) else date
|
||||
|
||||
|
||||
def get_month(date):
|
||||
''' Compute the month dates range on which the 'date' parameter belongs to.
|
||||
|
||||
:param date: A datetime.datetime or datetime.date object.
|
||||
:return: A tuple (date_from, date_to) having the same object type as the 'date' parameter.
|
||||
'''
|
||||
date_from = date_type(date)(date.year, date.month, 1)
|
||||
date_to = date_type(date)(date.year, date.month, calendar.monthrange(date.year, date.month)[1])
|
||||
return date_from, date_to
|
||||
|
||||
|
||||
def get_quarter_number(date):
|
||||
''' Get the number of the quarter on which the 'date' parameter belongs to.
|
||||
|
||||
:param date: A datetime.datetime or datetime.date object.
|
||||
:return: A [1-4] integer.
|
||||
'''
|
||||
return math.ceil(date.month / 3)
|
||||
|
||||
|
||||
def get_quarter(date):
|
||||
''' Compute the quarter dates range on which the 'date' parameter belongs to.
|
||||
|
||||
:param date: A datetime.datetime or datetime.date object.
|
||||
:return: A tuple (date_from, date_to) having the same object type as the 'date' parameter.
|
||||
'''
|
||||
quarter_number = get_quarter_number(date)
|
||||
month_from = ((quarter_number - 1) * 3) + 1
|
||||
date_from = date_type(date)(date.year, month_from, 1)
|
||||
date_to = (date_from + relativedelta(months=2))
|
||||
date_to = date_to.replace(day=calendar.monthrange(date_to.year, date_to.month)[1])
|
||||
return date_from, date_to
|
||||
|
||||
|
||||
def get_fiscal_year(date, day=31, month=12):
|
||||
''' Compute the fiscal year dates range on which the 'date' parameter belongs to.
|
||||
A fiscal year is the period used by governments for accounting purposes and vary between countries.
|
||||
By default, calling this method with only one parameter gives the calendar year because the ending date of the
|
||||
fiscal year is set to the YYYY-12-31.
|
||||
:param date: A datetime.datetime or datetime.date object.
|
||||
:param day: The day of month the fiscal year ends.
|
||||
:param month: The month of year the fiscal year ends.
|
||||
:return: A tuple (date_from, date_to) having the same object type as the 'date' parameter.
|
||||
'''
|
||||
|
||||
def fix_day(year, month, day):
|
||||
max_day = calendar.monthrange(year, month)[1]
|
||||
if month == 2 and day in (28, max_day):
|
||||
return max_day
|
||||
return min(day, max_day)
|
||||
|
||||
day = fix_day(date.year, month, day)
|
||||
date_to = date_type(date)(date.year, month, day)
|
||||
|
||||
if date <= date_to:
|
||||
date_from = date_to - relativedelta(years=1)
|
||||
day = fix_day(date_from.year, date_from.month, date_from.day)
|
||||
date_from = date_type(date)(date_from.year, date_from.month, day)
|
||||
date_from += relativedelta(days=1)
|
||||
else:
|
||||
date_from = date_to + relativedelta(days=1)
|
||||
date_to = date_to + relativedelta(years=1)
|
||||
day = fix_day(date_to.year, date_to.month, date_to.day)
|
||||
date_to = date_type(date)(date_to.year, date_to.month, day)
|
||||
return date_from, date_to
|
||||
|
||||
|
||||
def get_timedelta(qty, granularity):
|
||||
"""
|
||||
Helper to get a `relativedelta` object for the given quantity and interval unit.
|
||||
:param qty: the number of unit to apply on the timedelta to return
|
||||
:param granularity: Type of period in string, can be year, quarter, month, week, day or hour.
|
||||
|
||||
"""
|
||||
switch = {
|
||||
'hour': relativedelta(hours=qty),
|
||||
'day': relativedelta(days=qty),
|
||||
'week': relativedelta(weeks=qty),
|
||||
'month': relativedelta(months=qty),
|
||||
'year': relativedelta(years=qty),
|
||||
}
|
||||
return switch[granularity]
|
||||
|
||||
|
||||
def start_of(value, granularity):
|
||||
"""
|
||||
Get start of a time period from a date or a datetime.
|
||||
|
||||
:param value: initial date or datetime.
|
||||
:param granularity: type of period in string, can be year, quarter, month, week, day or hour.
|
||||
:return: a date/datetime object corresponding to the start of the specified period.
|
||||
"""
|
||||
is_datetime = isinstance(value, datetime)
|
||||
if granularity == "year":
|
||||
result = value.replace(month=1, day=1)
|
||||
elif granularity == "quarter":
|
||||
# Q1 = Jan 1st
|
||||
# Q2 = Apr 1st
|
||||
# Q3 = Jul 1st
|
||||
# Q4 = Oct 1st
|
||||
result = get_quarter(value)[0]
|
||||
elif granularity == "month":
|
||||
result = value.replace(day=1)
|
||||
elif granularity == 'week':
|
||||
# `calendar.weekday` uses ISO8601 for start of week reference, this means that
|
||||
# by default MONDAY is the first day of the week and SUNDAY is the last.
|
||||
result = value - relativedelta(days=calendar.weekday(value.year, value.month, value.day))
|
||||
elif granularity == "day":
|
||||
result = value
|
||||
elif granularity == "hour" and is_datetime:
|
||||
return datetime.combine(value, time.min).replace(hour=value.hour)
|
||||
elif is_datetime:
|
||||
raise ValueError(
|
||||
"Granularity must be year, quarter, month, week, day or hour for value %s" % value
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Granularity must be year, quarter, month, week or day for value %s" % value
|
||||
)
|
||||
|
||||
return datetime.combine(result, time.min) if is_datetime else result
|
||||
|
||||
|
||||
def end_of(value, granularity):
|
||||
"""
|
||||
Get end of a time period from a date or a datetime.
|
||||
|
||||
:param value: initial date or datetime.
|
||||
:param granularity: Type of period in string, can be year, quarter, month, week, day or hour.
|
||||
:return: A date/datetime object corresponding to the start of the specified period.
|
||||
"""
|
||||
is_datetime = isinstance(value, datetime)
|
||||
if granularity == "year":
|
||||
result = value.replace(month=12, day=31)
|
||||
elif granularity == "quarter":
|
||||
# Q1 = Mar 31st
|
||||
# Q2 = Jun 30th
|
||||
# Q3 = Sep 30th
|
||||
# Q4 = Dec 31st
|
||||
result = get_quarter(value)[1]
|
||||
elif granularity == "month":
|
||||
result = value + relativedelta(day=1, months=1, days=-1)
|
||||
elif granularity == 'week':
|
||||
# `calendar.weekday` uses ISO8601 for start of week reference, this means that
|
||||
# by default MONDAY is the first day of the week and SUNDAY is the last.
|
||||
result = value + relativedelta(days=6-calendar.weekday(value.year, value.month, value.day))
|
||||
elif granularity == "day":
|
||||
result = value
|
||||
elif granularity == "hour" and is_datetime:
|
||||
return datetime.combine(value, time.max).replace(hour=value.hour)
|
||||
elif is_datetime:
|
||||
raise ValueError(
|
||||
"Granularity must be year, quarter, month, week, day or hour for value %s" % value
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Granularity must be year, quarter, month, week or day for value %s" % value
|
||||
)
|
||||
|
||||
return datetime.combine(result, time.max) if is_datetime else result
|
||||
|
||||
|
||||
def add(value, *args, **kwargs):
|
||||
"""
|
||||
Return the sum of ``value`` and a :class:`relativedelta`.
|
||||
|
||||
:param value: initial date or datetime.
|
||||
:param args: positional args to pass directly to :class:`relativedelta`.
|
||||
:param kwargs: keyword args to pass directly to :class:`relativedelta`.
|
||||
:return: the resulting date/datetime.
|
||||
"""
|
||||
return value + relativedelta(*args, **kwargs)
|
||||
|
||||
|
||||
def subtract(value, *args, **kwargs):
|
||||
"""
|
||||
Return the difference between ``value`` and a :class:`relativedelta`.
|
||||
|
||||
:param value: initial date or datetime.
|
||||
:param args: positional args to pass directly to :class:`relativedelta`.
|
||||
:param kwargs: keyword args to pass directly to :class:`relativedelta`.
|
||||
:return: the resulting date/datetime.
|
||||
"""
|
||||
return value - relativedelta(*args, **kwargs)
|
||||
|
||||
def json_default(obj):
|
||||
"""
|
||||
Properly serializes date and datetime objects.
|
||||
"""
|
||||
from odoo import fields
|
||||
if isinstance(obj, datetime):
|
||||
return fields.Datetime.to_string(obj)
|
||||
if isinstance(obj, date):
|
||||
return fields.Date.to_string(obj)
|
||||
if isinstance(obj, lazy):
|
||||
return obj._value
|
||||
return ustr(obj)
|
||||
|
||||
|
||||
def date_range(start, end, step=relativedelta(months=1)):
|
||||
"""Date range generator with a step interval.
|
||||
|
||||
:param datetime start: beginning date of the range.
|
||||
:param datetime end: ending date of the range.
|
||||
:param relativedelta step: interval of the range.
|
||||
:return: a range of datetime from start to end.
|
||||
:rtype: Iterator[datetime]
|
||||
"""
|
||||
|
||||
are_naive = start.tzinfo is None and end.tzinfo is None
|
||||
are_utc = start.tzinfo == pytz.utc and end.tzinfo == pytz.utc
|
||||
|
||||
# Cases with miscellenous timezone are more complexe because of DST.
|
||||
are_others = start.tzinfo and end.tzinfo and not are_utc
|
||||
|
||||
if are_others:
|
||||
if start.tzinfo.zone != end.tzinfo.zone:
|
||||
raise ValueError("Timezones of start argument and end argument seem inconsistent")
|
||||
|
||||
if not are_naive and not are_utc and not are_others:
|
||||
raise ValueError("Timezones of start argument and end argument mismatch")
|
||||
|
||||
if start > end:
|
||||
raise ValueError("start > end, start date must be before end")
|
||||
|
||||
if start == start + step:
|
||||
raise ValueError("Looks like step is null")
|
||||
|
||||
if start.tzinfo:
|
||||
localize = start.tzinfo.localize
|
||||
else:
|
||||
localize = lambda dt: dt
|
||||
|
||||
dt = start.replace(tzinfo=None)
|
||||
end = end.replace(tzinfo=None)
|
||||
while dt <= end:
|
||||
yield localize(dt)
|
||||
dt = dt + step
|
||||
|
||||
|
||||
def weeknumber(locale: babel.Locale, date: date) -> Tuple[int, int]:
|
||||
"""Computes the year and weeknumber of `date`. The week number is 1-indexed
|
||||
(so the first week is week number 1).
|
||||
|
||||
For ISO locales (first day of week = monday, min week days = 4) the concept
|
||||
is clear and the Python stdlib implements it directly.
|
||||
|
||||
For other locales, it's basically nonsensical as there is no actual
|
||||
definition. For now we will implement non-split first-day-of-year, that is
|
||||
the first week of the year is the one which contains the first day of the
|
||||
year (taking first day of week in account), and the days of the previous
|
||||
year which are part of that week are considered to be in the next year for
|
||||
calendaring purposes.
|
||||
|
||||
That is December 27, 2015 is in the first week of 2016.
|
||||
|
||||
An alternative is to split the week in two, so the week from December 27,
|
||||
2015 to January 2, 2016 would be *both* W53/2015 and W01/2016.
|
||||
"""
|
||||
if locale.first_week_day == 0 and locale.min_week_days == 4:
|
||||
# woohoo nothing to do
|
||||
return date.isocalendar()[:2]
|
||||
|
||||
# first find the first day of the first week of the next year, if the
|
||||
# reference date is after that then it must be in the first week of the next
|
||||
# year, remove this if we decide to implement split weeks instead
|
||||
fdny = date.replace(year=date.year + 1, month=1, day=1) \
|
||||
- relativedelta(weekday=weekdays[locale.first_week_day](-1))
|
||||
if date >= fdny:
|
||||
return date.year + 1, 1
|
||||
|
||||
# otherwise get the number of periods of 7 days between the first day of the
|
||||
# first week and the reference
|
||||
fdow = date.replace(month=1, day=1) \
|
||||
- relativedelta(weekday=weekdays[locale.first_week_day](-1))
|
||||
doy = (date - fdow).days
|
||||
|
||||
return date.year, (doy // 7 + 1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue