Merge branch 'master' of gitlab.com:flectra-hq/flectra

This commit is contained in:
Flectra 2018-01-16 04:18:53 -08:00
commit a920b6819b
121 changed files with 2425 additions and 127 deletions

6
.gitignore vendored
View File

@ -15,10 +15,10 @@ __pycache__/
# hg stuff
*.orig
status
# odoo filestore
odoo/filestore
# flectra filestore
flectra/filestore
# maintenance migration scripts
odoo/addons/base/maintenance
flectra/addons/base/maintenance
# generated for windows installer?
install/win32/*.bat

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at flectra@flectrahq.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,6 +1,8 @@
Contributing to Odoo
Contributing to Flectra
====================
#TODO
[Full contribution guidelines](https://github.com/odoo/odoo/wiki/Contributing)
TL;DR

View File

@ -1,33 +1,17 @@
[![Build Status](http://runbot.odoo.com/runbot/badge/flat/1/11.0.svg)](http://runbot.odoo.com/runbot)
[![Tech Doc](http://img.shields.io/badge/11.0-docs-875A7B.svg?style=flat&colorA=8F8F8F)](http://www.odoo.com/documentation/11.0)
[![Help](http://img.shields.io/badge/11.0-help-875A7B.svg?style=flat&colorA=8F8F8F)](https://www.odoo.com/forum/help-1)
[![Nightly Builds](http://img.shields.io/badge/11.0-nightly-875A7B.svg?style=flat&colorA=8F8F8F)](http://nightly.odoo.com/)
Odoo
----
Flectra
=======
Odoo is a suite of web based open source business apps.
Flectra is a suite of web based open source business apps forked from Odoo.
The main Odoo Apps include an <a href="https://www.odoo.com/page/crm">Open Source CRM</a>,
<a href="https://www.odoo.com/page/website-builder">Website Builder</a>,
<a href="https://www.odoo.com/page/e-commerce">eCommerce</a>,
<a href="https://www.odoo.com/page/warehouse">Warehouse Management</a>,
<a href="https://www.odoo.com/page/project-management">Project Management</a>,
<a href="https://www.odoo.com/page/accounting">Billing &amp; Accounting</a>,
<a href="https://www.odoo.com/page/point-of-sale">Point of Sale</a>,
<a href="https://www.odoo.com/page/employees">Human Resources</a>,
<a href="https://www.odoo.com/page/lead-automation">Marketing</a>,
<a href="https://www.odoo.com/page/manufacturing">Manufacturing</a>,
<a href="https://www.odoo.com/page/purchase">Purchase Management</a>,
<a href="https://www.odoo.com/#apps">...</a>
The main Flectra Apps include an Open Source CRM,Website Builder,eCommerce,Warehouse Management,
Project Management,Billing &amp; Accounting,Point of Sale,Human Resources,Marketing,Manufacturing,
Purchase Management and many more.
Odoo Apps can be used as stand-alone applications, but they also integrate seamlessly so you get
a full-featured <a href="https://www.odoo.com">Open Source ERP</a> when you install several Apps.
Flectra Apps can be used as stand-alone applications, but they also integrate seamlessly so you get
a full-featured <a href="https://flectrahq.com">Open Source ERP</a> when you install several Apps.
Getting started with Odoo
-------------------------
For a standard installation please follow the <a href="https://www.odoo.com/documentation/11.0/setup/install.html">Setup instructions</a>
from the documentation.
Then follow <a href="https://www.odoo.com/documentation/11.0/tutorials.html">the developer tutorials</a>
Getting started with Flectra
----------------------------
For a standard installation please follow this <a href="https://gist.github.com/flectrahqadmin/d4c827577b3251a9505d3b697b1068be">gist</a>.

View File

@ -183,6 +183,12 @@
</div>
</div>
</div>
<t groups="account.group_account_user">
<h2>Fiscal Periods</h2>
<div class="row mt16 o_settings_container" id="accounting_reports">
<div class="col-xs-12 col-md-6 o_setting_box" id="fiscalyear" invisible="1"/>
</div>
</t>
<h2>Customer Payments</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" id="account_followup" title="This feature is useful if you issue a high amounts of invoices.">

View File

@ -30,10 +30,8 @@
</ul>
<p>Enjoy your Flectra experience,</p>
<div class="mt32">
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
<address>
For the Flectra Team,<br/>
Fabien Pinckaers, Founder
</address>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -2,6 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models, _
import json
class ResConfigSettings(models.TransientModel):
@ -35,17 +36,26 @@ class ResConfigSettings(models.TransientModel):
help="Allows to work in a multi currency environment")
paperformat_id = fields.Many2one(related="company_id.paperformat_id", string='Paper format')
external_report_layout = fields.Selection(related="company_id.external_report_layout")
send_statistics = fields.Boolean(
"Send Statistics")
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
params = self.env['ir.config_parameter'].sudo()
default_external_email_server = params.get_param('base_setup.default_external_email_server', default=False)
send_statistics = params._get_param(
'base_setup.send_statistics')
if send_statistics is None:
send_statistics = 'true'
if send_statistics in ['true', 'false']:
send_statistics = json.loads(send_statistics)
default_user_rights = params.get_param('base_setup.default_user_rights', default=False)
default_custom_report_footer = params.get_param('base_setup.default_custom_report_footer', default=False)
res.update(
default_external_email_server=default_external_email_server,
default_user_rights=default_user_rights,
send_statistics=send_statistics,
default_custom_report_footer=default_custom_report_footer,
company_share_partner=not self.env.ref('base.res_partner_rule').active,
)
@ -57,6 +67,11 @@ class ResConfigSettings(models.TransientModel):
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_external_email_server", self.default_external_email_server)
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_user_rights", self.default_user_rights)
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_custom_report_footer", self.default_custom_report_footer)
send_statistics = 'true'
if not self.send_statistics:
send_statistics = 'false'
self.env['ir.config_parameter'].sudo().set_param(
"base_setup.send_statistics", send_statistics)
self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner})
@api.multi

View File

@ -218,6 +218,20 @@
</div>
</div>
</div>
<h2>System Parameter</h2>
<div class="row mt16 o_settings_container" id="send_statistics">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="send_statistics" />
</div>
<div class="o_setting_right_pane">
<label for="send_statistics"/>
<div class="text-muted">
Enable/disable sending statistics to Flectra's Warranty Server.
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -9,9 +9,9 @@ from flectra.tools import pycompat
class BusController(Controller):
""" Examples:
openerp.jsonRpc('/longpolling/poll','call',{"channels":["c1"],last:0}).then(function(r){console.log(r)});
openerp.jsonRpc('/longpolling/send','call',{"channel":"c1","message":"m1"});
openerp.jsonRpc('/longpolling/send','call',{"channel":"c2","message":"m2"});
flectra.jsonRpc('/longpolling/poll','call',{"channels":["c1"],last:0}).then(function(r){console.log(r)});
flectra.jsonRpc('/longpolling/send','call',{"channel":"c1","message":"m1"});
flectra.jsonRpc('/longpolling/send','call',{"channel":"c2","message":"m2"});
"""
@route('/longpolling/send', type="json", auth="public")

View File

@ -121,7 +121,7 @@ class ImDispatch(object):
current._Thread__daemonic = True # PY2
current._daemonic = True # PY3
# rename the thread to avoid tests waiting for a longpolling
current.setName("openerp.longpolling.request.%s" % current.ident)
current.setName("flectra.longpolling.request.%s" % current.ident)
registry = flectra.registry(dbname)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -28,10 +28,8 @@
Have fun deploying your sales strategy,
</p>
<div class="mt32">
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
<address>
For the Flectra Team,<br/>
Fabien Pinckaers, Founder
</address>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -7,7 +7,7 @@
'version': '1.0',
'category': 'Extra Tools',
'description': """
The module adds the possibility to display data from Odoo in Google Spreadsheets in real time.
The module adds the possibility to display data from Flectra in Google Spreadsheets in real time.
=================================================================================================
""",
'depends': ['google_drive'],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -29,10 +29,8 @@
<p>Enjoy your Flectra experience,
</p>
<div class="mt32">
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
<address>
For the Flectra Team,<br/>
Fabien Pinckaers, Founder
</address>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -4,7 +4,7 @@
from flectra import http
from flectra.tools import config
from flectra.addons.web.controllers import main as web
from openerp.addons.hw_posbox_homepage.controllers import main as homepage
from flectra.addons.hw_posbox_homepage.controllers import main as homepage
import logging
import netifaces as ni

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,7 +1,7 @@
from openerp.modules.registry import RegistryManager
from flectra.modules.registry import RegistryManager
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
from flectra.addons.account.models.chart_template import migrate_tags_on_taxes
migrate_tags_on_taxes(cr, registry)

View File

@ -1,7 +1,7 @@
from openerp.modules.registry import RegistryManager
from flectra.modules.registry import RegistryManager
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
from flectra.addons.account.models.chart_template import migrate_tags_on_taxes
migrate_tags_on_taxes(cr, registry)

View File

@ -1,7 +1,7 @@
from openerp.modules.registry import RegistryManager
from flectra.modules.registry import RegistryManager
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_set_tags_and_taxes_updatable
from flectra.addons.account.models.chart_template import migrate_set_tags_and_taxes_updatable
migrate_set_tags_and_taxes_updatable(cr, registry, 'l10n_fr')

View File

@ -1,6 +1,6 @@
from openerp.modules.registry import RegistryManager
from flectra.modules.registry import RegistryManager
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
from flectra.addons.account.models.chart_template import migrate_tags_on_taxes
migrate_tags_on_taxes(cr, registry)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -36,6 +36,7 @@
'views/ir_actions_views.xml',
'views/ir_model_views.xml',
'views/res_partner_views.xml',
'wizard/mass_mail.xml',
],
'demo': [
'data/mail_demo.xml',

View File

@ -30,35 +30,41 @@ class PublisherWarrantyContract(AbstractModel):
db_create_date = IrParamSudo.get_param('database.create_date')
limit_date = datetime.datetime.now()
limit_date = limit_date - datetime.timedelta(15)
limit_date_str = limit_date.strftime(misc.DEFAULT_SERVER_DATETIME_FORMAT)
limit_date_str = limit_date.strftime(
misc.DEFAULT_SERVER_DATETIME_FORMAT)
nbr_users = Users.search_count([('active', '=', True)])
nbr_active_users = Users.search_count([("login_date", ">=", limit_date_str), ('active', '=', True)])
nbr_active_users = Users.search_count(
[("login_date", ">=", limit_date_str), ('active', '=', True)])
nbr_share_users = 0
nbr_active_share_users = 0
if "share" in Users._fields:
nbr_share_users = Users.search_count([("share", "=", True), ('active', '=', True)])
nbr_active_share_users = Users.search_count([("share", "=", True), ("login_date", ">=", limit_date_str), ('active', '=', True)])
nbr_share_users = Users.search_count(
[("share", "=", True), ('active', '=', True)])
nbr_active_share_users = Users.search_count(
[("share", "=", True), ("login_date", ">=", limit_date_str),
('active', '=', True)])
user = self.env.user
domain = [('application', '=', True), ('state', 'in', ['installed', 'to upgrade', 'to remove'])]
domain = [('application', '=', True),
('state', 'in', ['installed', 'to upgrade', 'to remove'])]
apps = self.env['ir.module.module'].sudo().search_read(domain, ['name'])
enterprise_code = IrParamSudo.get_param('database.enterprise_code')
demo_domain = [('name', 'ilike', 'base'), ('demo', '=', True)]
demo_data_ids = self.env['ir.module.module'].sudo().search(demo_domain)
demo_data = True
if not demo_data_ids:
demo_data = False
support_code = IrParamSudo.get_param('database.support_code')
web_base_url = IrParamSudo.get_param('web.base.url')
msg = {
"dbuuid": dbuuid,
"nbr_users": nbr_users,
msg = {"dbuuid": dbuuid, "nbr_users": nbr_users,
"nbr_active_users": nbr_active_users,
"nbr_share_users": nbr_share_users,
"nbr_active_share_users": nbr_active_share_users,
"dbname": self._cr.dbname,
"db_create_date": db_create_date,
"version": release.version,
"language": user.lang,
"dbname": self._cr.dbname, "db_create_date": db_create_date,
"version": release.version, "language": user.lang,
"web_base_url": web_base_url,
"apps": [app['name'] for app in apps],
"enterprise_code": enterprise_code,
}
"support_code": support_code,
"demo_data": demo_data}
if user.partner_id.company_id:
company_id = user.partner_id.company_id
msg.update(company_id.read(["name", "email", "phone"])[0])
@ -88,13 +94,20 @@ class PublisherWarrantyContract(AbstractModel):
@type cron_mode: boolean
"""
try:
# Code will be execute only if parameter value 'True'
parameter_id = self.env['ir.config_parameter'].sudo().get_param(
'base_setup.send_statistics')
if parameter_id != 'true':
return True
try:
result = self._get_sys_logs()
except Exception:
if cron_mode: # we don't want to see any stack trace in cron
return False
_logger.debug("Exception while sending a get logs messages", exc_info=1)
raise UserError(_("Error during communication with the publisher warranty server."))
_logger.debug("Exception while sending a get logs messages",
exc_info=1)
raise UserError(_(
"Error during communication with the publisher warranty server."))
# old behavior based on res.log; now on mail.message, that is not necessarily installed
user = self.env['res.users'].sudo().browse(SUPERUSER_ID)
poster = self.sudo().env.ref('mail.channel_all_employees')
@ -104,15 +117,20 @@ class PublisherWarrantyContract(AbstractModel):
poster = user
for message in result["messages"]:
try:
poster.message_post(body=message, subtype='mt_comment', partner_ids=[user.partner_id.id])
poster.message_post(body=message, subtype='mt_comment',
partner_ids=[user.partner_id.id])
except Exception:
pass
if result.get('enterprise_info'):
if result.get('support_info'):
# Update expiration date
set_param = self.env['ir.config_parameter'].sudo().set_param
set_param('database.expiration_date', result['enterprise_info'].get('expiration_date'))
set_param('database.expiration_reason', result['enterprise_info'].get('expiration_reason', 'trial'))
set_param('database.enterprise_code', result['enterprise_info'].get('enterprise_code'))
set_param('database.expiration_date',
result['support_info'].get('expiration_date'))
set_param('database.expiration_reason',
result['support_info'].get('expiration_reason',
'trial'))
set_param('database.support_code',
result['support_info'].get('support_code'))
except Exception:
if cron_mode:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="WebClient.announcement_bar">
<div class="openerp" id="announcement_bar_table">
<table class="oe_webclient">
<tr>
<td colspan="2" class="announcement_bar">
<span class="message"></span>
<span class="url">
<a href="https://services.openerp.com/openerp-enterprise/ab/register" target="_blank"></a>
</span>
<span class="close"></span>
</td>
</tr>
</table>
</div>
</t>
</templates>

View File

@ -5,3 +5,5 @@ from . import invite
from . import mail_compose_message
from . import email_template_preview
from . import base_module_uninstall
from . import mass_mail

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models
class MassMail(models.TransientModel):
_name = 'mass.mail'
_description = 'Mass Mail Options'
message = fields.Char('Message')
@api.multi
def mass_emails(self):
send_method = self.env.context['mail']
active_ids = self.env.context['active_ids']
lines = self.env['mail.mail'].search([( 'id', 'in', active_ids)])
if send_method == 'mark_outgoing':
lines.mark_outgoing()
elif send_method == 'send':
lines.send()
elif send_method == 'cancel':
lines.cancel()

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<flectra>
<data>
<record id="view_mass_retry_mail_form" model="ir.ui.view">
<field name="name">mass.retry.mail.form</field>
<field name="model">mass.mail</field>
<field name="arch" type="xml">
<form string="Retry">
<h2>Are you sure?</h2>
<footer>
<span groups="base.group_user">
<button string="Yes" type="object"
name="mass_emails" class="oe_highlight"/>
or
<button special="cancel" string="No" type="object" />
</span>
</footer>
</form>
</field>
</record>
<act_window id="action_mass_retry_mail_view" multi="True"
context="{'mail': 'mark_outgoing'}"
name="Retry" res_model="mass.mail" src_model="mail.mail"
view_mode="form" target="new" view_type="form" />
<record id="view_mass_resend_mail_form" model="ir.ui.view">
<field name="name">mass.resend.mail.form</field>
<field name="model">mass.mail</field>
<field name="arch" type="xml">
<form string="Resend">
<h2>Are you sure?</h2>
<footer>
<span groups="base.group_user">
<button string="Yes" type="object"
name="mass_emails" class="oe_highlight"/>
or
<button special="cancel" string="No" type="object" />
</span>
</footer>
</form>
</field>
</record>
<act_window id="action_mass_resend_mail_view" multi="True"
context="{'mail': 'send'}"
name="Resend" res_model="mass.mail" src_model="mail.mail"
view_mode="form" target="new" view_type="form" />
<record id="view_mass_cancel_mail_form" model="ir.ui.view">
<field name="name">mass.cancel.mail.form</field>
<field name="model">mass.mail</field>
<field name="arch" type="xml">
<form string="Cancel">
<h2>Are you sure?</h2>
<footer>
<span groups="base.group_user">
<button string="Yes" type="object"
name="mass_emails" class="oe_highlight"/>
or
<button special="cancel" string="No" type="object" />
</span>
</footer>
</form>
</field>
</record>
<act_window id="action_mass_cancel_mail_view" multi="True"
context="{'mail': 'cancel'}"
name="Cancel" res_model="mass.mail" src_model="mail.mail"
view_mode="form" target="new" view_type="form" />
</data>
</flectra>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,63 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
=================
Password Security
=================
This module allows admin to set company-level password security requirements
and enforces them on the user.
It contains features such as
* Password expiration days
* Password length requirement
* Password minimum number of lowercase letters
* Password minimum number of uppercase letters
* Password minimum number of numbers
* Password minimum number of special characters
Configuration
=============
# Navigate to company you would like to set requirements on
# Click the ``Password Policy`` page
# Set the policies to your liking.
Password complexity requirements will be enforced upon next password change for
any user in that company.
Settings & Defaults
-------------------
These are defined at the company level:
===================== ======= ===================================================
Name Default Description
===================== ======= ===================================================
password_expiration 60 Days until passwords expire
password_length 12 Minimum number of characters in password
password_lower 0 Minimum number of lowercase letter in password
password_upper 0 Minimum number of uppercase letters in password
password_numeric 0 Minimum number of number in password
password_special 0 Minimum number of unique special character in password
password_history 30 Disallow reuse of this many previous passwords
password_minimum 24 Amount of hours that must pass until another reset
===================== ======= ===================================================
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* James Foster <jfoster@laslabs.com>
* Dave Lasley <dave@laslabs.com>

View File

@ -0,0 +1,5 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import controllers
from . import models

View File

@ -0,0 +1,25 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
'name': 'Password Security',
"summary": "Allow admin to set password security requirements.",
'version': '11.0.1.0.0',
'author': "LasLabs, Odoo Community Association (OCA), FlectraHQ",
'category': 'Base',
'depends': [
'auth_crypt',
'auth_signup',
],
"website": "https://laslabs.com",
"license": "LGPL-3",
"data": [
'views/res_company_view.xml',
'security/ir.model.access.csv',
'security/res_users_pass_history.xml',
],
"demo": [
'demo/res_users.xml',
],
'installable': True,
}

View File

@ -0,0 +1,4 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import main

View File

@ -0,0 +1,93 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import operator
from flectra import http
from flectra.http import request
from flectra.addons.auth_signup.controllers.main import AuthSignupHome
from flectra.addons.web.controllers.main import ensure_db, Session
from ..exceptions import PassError
class PasswordSecuritySession(Session):
@http.route()
def change_password(self, fields):
new_password = operator.itemgetter('new_password')(
dict(map(operator.itemgetter('name', 'value'), fields))
)
user_id = request.env.user
user_id._check_password(new_password)
return super(PasswordSecuritySession, self).change_password(fields)
class PasswordSecurityHome(AuthSignupHome):
def do_signup(self, qcontext):
password = qcontext.get('password')
user_id = request.env.user
user_id._check_password(password)
return super(PasswordSecurityHome, self).do_signup(qcontext)
@http.route()
def web_login(self, *args, **kw):
ensure_db()
response = super(PasswordSecurityHome, self).web_login(*args, **kw)
if not request.httprequest.method == 'POST':
return response
uid = request.session.authenticate(
request.session.db,
request.params['login'],
request.params['password']
)
if not uid:
return response
users_obj = request.env['res.users'].sudo()
user_id = users_obj.browse(request.uid)
if not user_id._password_has_expired():
return response
user_id.action_expire_password()
request.session.logout(keep_db=True)
redirect = user_id.partner_id.signup_url
return http.redirect_with_hash(redirect)
@http.route()
def web_auth_signup(self, *args, **kw):
try:
return super(PasswordSecurityHome, self).web_auth_signup(
*args, **kw
)
except PassError as e:
qcontext = self.get_auth_signup_qcontext()
qcontext['error'] = e.message
return request.render('auth_signup.signup', qcontext)
@http.route()
def web_auth_reset_password(self, *args, **kw):
""" It provides hook to disallow front-facing resets inside of min
Unfortuantely had to reimplement some core logic here because of
nested logic in parent
"""
qcontext = self.get_auth_signup_qcontext()
if (
request.httprequest.method == 'POST' and
qcontext.get('login') and
'error' not in qcontext and
'token' not in qcontext
):
login = qcontext.get('login')
user_ids = request.env.sudo().search(
[('login', '=', login)],
limit=1,
)
if not user_ids:
user_ids = request.env.sudo().search(
[('email', '=', login)],
limit=1,
)
user_ids._validate_pass_reset()
return super(PasswordSecurityHome, self).web_auth_reset_password(
*args, **kw
)

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<flectra>
<record id="base.user_root" model="res.users">
<field name="password_write_date"
eval="datetime.now()"
/>
</record>
</flectra>

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from flectra.exceptions import Warning as UserError
class PassError(UserError):
""" Example: When you try to create an insecure password."""
def __init__(self, msg):
self.message = msg
super(PassError, self).__init__(msg)

View File

@ -0,0 +1,6 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import res_users
from . import res_company
from . import res_users_pass_history

View File

@ -0,0 +1,47 @@
# Copyright 2015 LasLabs Inc.
# Copyright 2004-TODAY FlectraHQ.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from flectra import models, fields
class ResCompany(models.Model):
_inherit = 'res.company'
password_expiration = fields.Integer(
'Days',
default=60,
help='How many days until passwords expire',
)
password_length = fields.Integer(
'Characters',
default=12,
help='Minimum number of characters',
)
password_lower = fields.Integer(
'Lowercase',
help='Require lowercase letters',
)
password_upper = fields.Integer(
'Uppercase',
help='Require uppercase letters',
)
password_numeric = fields.Integer(
'Numeric',
help='Require numeric digits',
)
password_special = fields.Integer(
'Special',
help='Require unique special characters',
)
password_history = fields.Integer(
'History',
default=30,
help='Disallow reuse of this many previous passwords - use negative '
'number for infinite, or 0 to disable',
)
password_minimum = fields.Integer(
'Minimum Hours',
default=24,
help='Amount of hours until a user may change password again',
)

View File

@ -0,0 +1,162 @@
# Copyright 2015 LasLabs Inc.
# Copyright 2004-TODAY FlectraHQ.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import re
from datetime import datetime, timedelta
from flectra import api, fields, models, _
from ..exceptions import PassError
def delta_now(**kwargs):
dt = datetime.now() + timedelta(**kwargs)
return fields.Datetime.to_string(dt)
class ResUsers(models.Model):
_inherit = 'res.users'
password_write_date = fields.Datetime(
'Last password update',
readonly=True,
)
password_history_ids = fields.One2many(
string='Password History',
comodel_name='res.users.pass.history',
inverse_name='user_id',
readonly=True,
)
@api.model
def create(self, vals):
vals['password_write_date'] = fields.Datetime.now()
return super(ResUsers, self).create(vals)
@api.multi
def write(self, vals):
if vals.get('password'):
self._check_password(vals['password'])
vals['password_write_date'] = fields.Datetime.now()
return super(ResUsers, self).write(vals)
@api.multi
def _check_password(self, password):
self._check_password_rules(password)
self._check_password_history(password)
return True
@api.multi
def _check_password_rules(self, password):
self.ensure_one()
if not password:
return True
company_id = self.company_id
message = []
if company_id.password_lower and sum(map(str.islower, password)) < \
company_id.password_lower:
message.append('\n ' + _('Lowercase letter (At least ' +
str(company_id.password_lower) +
' character)')
)
if company_id.password_upper and sum(map(str.isupper, password)) < \
company_id.password_upper:
message.append('\n ' + _('Uppercase letter (At least ' +
str(company_id.password_upper) +
' character)')
)
if company_id.password_numeric and sum(map(str.isdigit, password)) < \
company_id.password_numeric:
message.append('\n ' + _('Numeric digit (At least ' +
str(company_id.password_numeric) +
' numeric)')
)
if company_id.password_special and len(set('[~!@#$%^&*()_+{}":;\']+$'
).intersection(
password)) < company_id.password_numeric:
message.append('\n ' + _('Special character (At least ' +
str(company_id.password_special) +
' character of [ ~ ! @ # $ % ^ & * ( )_+ {'
' } " : ; \' ])')
)
if company_id.password_length and len(password) < \
company_id.password_length:
message = [_('Password must be %d characters or more.') %
company_id.password_length] + message
if len(message) > 0:
raise PassError('\r'.join(message))
else:
return True
@api.multi
def _password_has_expired(self):
self.ensure_one()
if not self.password_write_date:
return True
write_date = fields.Datetime.from_string(self.password_write_date)
today = fields.Datetime.from_string(fields.Datetime.now())
days = (today - write_date).days
return days > self.company_id.password_expiration
@api.multi
def action_expire_password(self):
expiration = delta_now(days=+1)
for rec_id in self:
rec_id.mapped('partner_id').signup_prepare(
signup_type="reset", expiration=expiration
)
@api.multi
def _validate_pass_reset(self):
""" It provides validations before initiating a pass reset email
:raises: PassError on invalidated pass reset attempt
:return: True on allowed reset
"""
for rec_id in self:
pass_min = rec_id.company_id.password_minimum
if pass_min <= 0:
pass
write_date = fields.Datetime.from_string(
rec_id.password_write_date
)
delta = timedelta(hours=pass_min)
if write_date + delta > datetime.now():
raise PassError(
_('Passwords can only be reset every %d hour(s). '
'Please contact an administrator for assistance.') %
pass_min,
)
return True
@api.multi
def _check_password_history(self, password):
""" It validates proposed password against existing history
:raises: PassError on reused password
"""
crypt = self._crypt_context()
for rec_id in self:
recent_passes = rec_id.company_id.password_history
if recent_passes < 0:
recent_passes = rec_id.password_history_ids
else:
recent_passes = rec_id.password_history_ids[
0:recent_passes-1
]
if recent_passes.filtered(
lambda r: crypt.verify(password, r.password_crypt)):
raise PassError(
_('Cannot use the most recent %d passwords') %
rec_id.company_id.password_history
)
@api.multi
def _set_encrypted_password(self, encrypted):
""" It saves password crypt history for history rules """
super(ResUsers, self)._set_encrypted_password(encrypted)
self.write({
'password_history_ids': [(0, 0, {
'password_crypt': encrypted,
})],
})

View File

@ -0,0 +1,25 @@
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from flectra import fields, models
class ResUsersPassHistory(models.Model):
_name = 'res.users.pass.history'
_description = 'Res Users Password History'
_order = 'user_id, date desc'
user_id = fields.Many2one(
string='User',
comodel_name='res.users',
ondelete='cascade',
index=True,
)
password_crypt = fields.Char(
string='Encrypted Password',
)
date = fields.Datetime(
default=lambda s: fields.Datetime.now(),
index=True,
)

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_res_users_pass_history,access_res_users_pass_history,model_res_users_pass_history,base.group_user,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_res_users_pass_history access_res_users_pass_history model_res_users_pass_history base.group_user 1 0 1 0

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<flectra>
<record id="res_users_pass_history_rule" model="ir.rule">
<field name="name">Res Users Pass History Access</field>
<field name="model_id"
ref="password_security.model_res_users_pass_history"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">[
('user_id', '=', user.id)
]</field>
</record>
</flectra>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_res_users
from . import test_password_security_home
from . import test_password_security_session

View File

@ -0,0 +1,281 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import mock
from contextlib import contextmanager
from flectra.tests.common import TransactionCase
from flectra.http import Response
from ..controllers import main
IMPORT = 'flectra.addons.password_security.controllers.main'
class EndTestException(Exception):
""" It allows for isolation of resources by raise """
class MockResponse(object):
def __new__(cls):
return mock.Mock(spec=Response)
class MockPassError(main.PassError):
def __init__(self):
super(MockPassError, self).__init__('Message')
class TestPasswordSecurityHome(TransactionCase):
def setUp(self):
super(TestPasswordSecurityHome, self).setUp()
self.PasswordSecurityHome = main.PasswordSecurityHome
self.password_security_home = self.PasswordSecurityHome()
self.passwd = 'I am a password!'
self.qcontext = {
'password': self.passwd,
}
@contextmanager
def mock_assets(self):
""" It mocks and returns assets used by this controller """
methods = ['do_signup', 'web_login', 'web_auth_signup',
'web_auth_reset_password',
]
with mock.patch.multiple(
main.AuthSignupHome, **{m: mock.DEFAULT for m in methods}
) as _super:
mocks = {}
for method in methods:
mocks[method] = _super[method]
mocks[method].return_value = MockResponse()
with mock.patch('%s.request' % IMPORT) as request:
with mock.patch('%s.ensure_db' % IMPORT) as ensure:
with mock.patch('%s.http' % IMPORT) as http:
http.redirect_with_hash.return_value = \
MockResponse()
mocks.update({
'request': request,
'ensure_db': ensure,
'http': http,
})
yield mocks
def test_do_signup_check(self):
""" It should check password on user """
with self.mock_assets() as assets:
check_password = assets['request'].env.user._check_password
check_password.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.do_signup(self.qcontext)
check_password.assert_called_once_with(
self.passwd,
)
def test_do_signup_return(self):
""" It should return result of super """
with self.mock_assets() as assets:
res = self.password_security_home.do_signup(self.qcontext)
self.assertEqual(assets['do_signup'](), res)
def test_web_login_ensure_db(self):
""" It should verify available db """
with self.mock_assets() as assets:
assets['ensure_db'].side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
def test_web_login_super(self):
""" It should call superclass w/ proper args """
expect_list = [1, 2, 3]
expect_dict = {'test1': 'good1', 'test2': 'good2'}
with self.mock_assets() as assets:
assets['web_login'].side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login(
*expect_list, **expect_dict
)
assets['web_login'].assert_called_once_with(
*expect_list, **expect_dict
)
def test_web_login_no_post(self):
""" It should return immediate result of super when not POST """
with self.mock_assets() as assets:
assets['request'].httprequest.method = 'GET'
assets['request'].session.authenticate.side_effect = \
EndTestException
res = self.password_security_home.web_login()
self.assertEqual(
assets['web_login'](), res,
)
def test_web_login_authenticate(self):
""" It should attempt authentication to obtain uid """
with self.mock_assets() as assets:
assets['request'].httprequest.method = 'POST'
authenticate = assets['request'].session.authenticate
request = assets['request']
authenticate.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
authenticate.assert_called_once_with(
request.session.db,
request.params['login'],
request.params['password'],
)
def test_web_login_authenticate_fail(self):
""" It should return super result if failed auth """
with self.mock_assets() as assets:
authenticate = assets['request'].session.authenticate
request = assets['request']
request.httprequest.method = 'POST'
request.env['res.users'].sudo.side_effect = EndTestException
authenticate.return_value = False
res = self.password_security_home.web_login()
self.assertEqual(
assets['web_login'](), res,
)
def test_web_login_get_user(self):
""" It should get the proper user as sudo """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
sudo = request.env['res.users'].sudo()
sudo.browse.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
sudo.browse.assert_called_once_with(
request.uid
)
def test_web_login_valid_pass(self):
""" It should return parent result if pass isn't expired """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user.action_expire_password.side_effect = EndTestException
user._password_has_expired.return_value = False
res = self.password_security_home.web_login()
self.assertEqual(
assets['web_login'](), res,
)
def test_web_login_expire_pass(self):
""" It should expire password if necessary """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user.action_expire_password.side_effect = EndTestException
user._password_has_expired.return_value = True
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
def test_web_login_log_out_if_expired(self):
"""It should log out user if password expired"""
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user._password_has_expired.return_value = True
self.password_security_home.web_login()
logout_mock = request.session.logout
logout_mock.assert_called_once_with(keep_db=True)
def test_web_login_redirect(self):
""" It should redirect w/ hash to reset after expiration """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user._password_has_expired.return_value = True
res = self.password_security_home.web_login()
self.assertEqual(
assets['http'].redirect_with_hash(), res,
)
def test_web_auth_signup_valid(self):
""" It should return super if no errors """
with self.mock_assets() as assets:
res = self.password_security_home.web_auth_signup()
self.assertEqual(
assets['web_auth_signup'](), res,
)
def test_web_auth_signup_invalid_qcontext(self):
""" It should catch PassError and get signup qcontext """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext',
) as qcontext:
assets['web_auth_signup'].side_effect = MockPassError
qcontext.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_auth_signup()
def test_web_auth_signup_invalid_render(self):
""" It should render & return signup form on invalid """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
assets['web_auth_signup'].side_effect = MockPassError
res = self.password_security_home.web_auth_signup()
assets['request'].render.assert_called_once_with(
'auth_signup.signup', qcontext(),
)
self.assertEqual(
assets['request'].render(), res,
)
def test_web_auth_reset_password_fail_login(self):
""" It should raise from failed _validate_pass_reset by login """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
qcontext['login'] = 'login'
search = assets['request'].env.sudo().search
assets['request'].httprequest.method = 'POST'
user = mock.MagicMock()
user._validate_pass_reset.side_effect = MockPassError
search.return_value = user
with self.assertRaises(MockPassError):
self.password_security_home.web_auth_reset_password()
def test_web_auth_reset_password_fail_email(self):
""" It should raise from failed _validate_pass_reset by email """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
qcontext['login'] = 'login'
search = assets['request'].env.sudo().search
assets['request'].httprequest.method = 'POST'
user = mock.MagicMock()
user._validate_pass_reset.side_effect = MockPassError
search.side_effect = [[], user]
with self.assertRaises(MockPassError):
self.password_security_home.web_auth_reset_password()
def test_web_auth_reset_password_success(self):
""" It should return parent response on no validate errors """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
qcontext['login'] = 'login'
assets['request'].httprequest.method = 'POST'
res = self.password_security_home.web_auth_reset_password()
self.assertEqual(
assets['web_auth_reset_password'](), res,
)

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import mock
from contextlib import contextmanager
from flectra.tests.common import TransactionCase
from ..controllers import main
IMPORT = 'flectra.addons.password_security.controllers.main'
class EndTestException(Exception):
""" It allows for isolation of resources by raise """
class TestPasswordSecuritySession(TransactionCase):
def setUp(self):
super(TestPasswordSecuritySession, self).setUp()
self.PasswordSecuritySession = main.PasswordSecuritySession
self.password_security_session = self.PasswordSecuritySession()
self.passwd = 'I am a password!'
self.fields = [
{'name': 'new_password', 'value': self.passwd},
]
@contextmanager
def mock_assets(self):
""" It mocks and returns assets used by this controller """
with mock.patch('%s.request' % IMPORT) as request:
yield {
'request': request,
}
def test_change_password_check(self):
""" It should check password on request user """
with self.mock_assets() as assets:
check_password = assets['request'].env.user._check_password
check_password.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_session.change_password(self.fields)
check_password.assert_called_once_with(
self.passwd,
)
def test_change_password_return(self):
""" It should return result of super """
with self.mock_assets():
with mock.patch.object(main.Session, 'change_password') as chg:
res = self.password_security_session.change_password(
self.fields
)
self.assertEqual(chg(), res)

View File

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import time
from flectra.tests.common import TransactionCase
from ..exceptions import PassError
class TestResUsers(TransactionCase):
def setUp(self):
super(TestResUsers, self).setUp()
self.login = 'foslabs@example.com'
self.partner_vals = {
'name': 'Partner',
'is_company': False,
'email': self.login,
}
self.password = 'asdQWE123$%^'
self.main_comp = self.env.ref('base.main_company')
self.vals = {
'name': 'User',
'login': self.login,
'password': self.password,
'company_id': self.main_comp.id
}
self.model_obj = self.env['res.users']
def _new_record(self):
partner_id = self.env['res.partner'].create(self.partner_vals)
self.vals['partner_id'] = partner_id.id
return self.model_obj.create(self.vals)
def test_password_write_date_is_saved_on_create(self):
rec_id = self._new_record()
self.assertTrue(
rec_id.password_write_date,
'Password write date was not saved to db.',
)
def test_password_write_date_is_updated_on_write(self):
rec_id = self._new_record()
old_write_date = rec_id.password_write_date
time.sleep(2)
rec_id.write({'password': 'asdQWE123$%^2'})
rec_id.refresh()
new_write_date = rec_id.password_write_date
self.assertNotEqual(
old_write_date, new_write_date,
'Password write date was not updated on write.',
)
def test_does_not_update_write_date_if_password_unchanged(self):
rec_id = self._new_record()
old_write_date = rec_id.password_write_date
time.sleep(2)
rec_id.write({'name': 'Luser'})
rec_id.refresh()
new_write_date = rec_id.password_write_date
self.assertEqual(
old_write_date, new_write_date,
'Password not changed but write date updated anyway.',
)
def test_check_password_returns_true_for_valid_password(self):
rec_id = self._new_record()
self.assertTrue(
rec_id._check_password('asdQWE123$%^3'),
'Password is valid but check failed.',
)
def test_check_password_raises_error_for_invalid_password(self):
rec_id = self._new_record()
with self.assertRaises(PassError):
rec_id._check_password('password')
def test_save_password_crypt(self):
rec_id = self._new_record()
self.assertEqual(
1, len(rec_id.password_history_ids),
)
def test_check_password_crypt(self):
""" It should raise PassError if previously used """
rec_id = self._new_record()
with self.assertRaises(PassError):
rec_id.write({'password': self.password})
def test_password_is_expired_if_record_has_no_write_date(self):
rec_id = self._new_record()
rec_id.write({'password_write_date': None})
rec_id.refresh()
self.assertTrue(
rec_id._password_has_expired(),
'Record has no password write date but check failed.',
)
def test_an_old_password_is_expired(self):
rec_id = self._new_record()
old_write_date = '1970-01-01 00:00:00'
rec_id.write({'password_write_date': old_write_date})
rec_id.refresh()
self.assertTrue(
rec_id._password_has_expired(),
'Password is out of date but check failed.',
)
def test_a_new_password_is_not_expired(self):
rec_id = self._new_record()
self.assertFalse(
rec_id._password_has_expired(),
'Password was just created but has already expired.',
)
def test_expire_password_generates_token(self):
rec_id = self._new_record()
rec_id.sudo().action_expire_password()
rec_id.refresh()
token = rec_id.partner_id.signup_token
self.assertTrue(
token,
'A token was not generated.',
)
def test_validate_pass_reset_error(self):
""" It should throw PassError on reset inside min threshold """
rec_id = self._new_record()
with self.assertRaises(PassError):
rec_id._validate_pass_reset()
def test_validate_pass_reset_allow(self):
""" It should allow reset pass when outside threshold """
rec_id = self._new_record()
rec_id.password_write_date = '2016-01-01'
self.assertEqual(
True, rec_id._validate_pass_reset(),
)
def test_validate_pass_reset_zero(self):
""" It should allow reset pass when <= 0 """
rec_id = self._new_record()
rec_id.company_id.password_minimum = 0
self.assertEqual(
True, rec_id._validate_pass_reset(),
)

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2015 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<flectra>
<record id="view_company_form" model="ir.ui.view">
<field name="name">res.company.form</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Password Policy">
<group>
<group string="Timings">
<field name="password_expiration" />
<field name="password_minimum" />
</group>
<group string="Extra">
<field name="password_length" />
<field name="password_history" />
</group>
</group>
<group name="chars_grp" string="Required Characters">
<group>
<field name="password_lower" />
<field name="password_upper" />
</group>
<group>
<field name="password_numeric" />
<field name="password_special" />
</group>
</group>
</page>
</xpath>
</field>
</record>
</flectra>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -278,7 +278,7 @@
<field name="project_id" ref="project.project_project_3"/>
<field name="name">User Interface design</field>
<field name="stage_id" ref="project_stage_2"/>
<field name="date_start">2011-02-06</field>
<field name="date_start">2016-08-25</field>
<field name="tag_ids" eval="[(6, 0, [
ref('project_tags_03')])]"/>
</record>

View File

@ -25,10 +25,8 @@
Good luck!
</p></i>
<div class="mt32">
<img class="signature" src="/web_planner/static/src/img/fabien_signature.png"/>
<address>
For the Flectra Team,<br/>
Fabien Pinckaers, Founder
</address>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -627,6 +627,14 @@
</field>
</record>
<record id="view_project_task_gantt" model="ir.ui.view">
<field name="name">project.task.gantt</field>
<field name="model">project.task</field>
<field name="arch" type="xml">
<gantt date_start="date_start" date_stop="date_end" string="Tasks"/>
</field>
</record>
<record id="analytic_account_inherited_form" model="ir.ui.view">
<field name="name">account.analytic.account.form.inherit</field>
<field name="model">account.analytic.account</field>
@ -645,7 +653,7 @@
<record id="action_view_task" model="ir.actions.act_window">
<field name="name">Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,gantt</field>
<field name="context">{'search_default_my_tasks': 1}</field>
<field name="search_view_id" ref="view_task_search_form"/>
<field name="help" type="html">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -2,8 +2,8 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from functools import partial
import openerp
from openerp import api, SUPERUSER_ID
import flectra
from flectra import api, SUPERUSER_ID
from . import models # noqa
from . import report # noqa

View File

@ -22,10 +22,8 @@
to inventory reduction and better efficiencies in your daily operations.
</p>
<div class="mt32">
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
<address>
For the Flectra Team,<br/>
Fabien Pinckaers, Founder
</address>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,73 @@
jQuery.ganttView
================
The jQuery.ganttView plugin is a very lightweight plugin for creating a Gantt chart in plain HTML...no vector graphics or images required. The plugin supports dragging and resizing the Gantt blocks and callbacks to trap the updated data.
[![Sample Gantt](https://raw.githubusercontent.com/thegrubbsian/jquery.ganttView/master/example/jquery-ganttview.png) A sample chart](http://thegrubbsian.github.io/jquery.ganttView/example/index.html)
Browser Compatibility
---------------------
Currently the plugin has been tested, and is working in: FF 3.5+, Chrome 5+, Safari 4+, IE8+. There are minor issues in IE7 and I haven't even attempted to use it in IE6. If you encounter any issues with any version of Internet Explorer and would like to contribute CSS fixes please do so, several people have asked for IE6 support.
Dependencies
------------
The plugin depends on the following libraries:
- jQuery 1.7 or higher
- jQuery-UI 1.8 or higher
- date.js
Documentation
-------------
Forthcoming...
Contribution Guidelines
------------
The internal roadmap for the plugin is detailed in the project wiki. If you're interested in features outside of what's described there, we'd be interested to discuss pull requests that would add these features. If you like the plugin, feel free to fork it and submit your patches back.
**Guidelines:** If you'd like to offer a new feature please help us out by submitting the pull request with only the fewest changes necessary.
Ideal: Fork the project, apply just the individual changes to the individual files effected, submit pull request.
Those pull requests can usually be automatically merged and closed through the site here.
If your pull request includes things like:
- changes to dependencies or where they're hosted
- stylistic modifications
- moving project files to different directories
- more than one new feature / functional change
one of us will have to do the work of carving out just the feature being pulled. Your request is likely to sit unmerged for a while if that's the case.
License
-------
The jQuery.ganttView plugin may be used free of charge under the conditions
of the following license:
The MIT License
Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,145 @@
/**
* @version: 1.0 Alpha-1
* @author: Coolite Inc. http://www.coolite.com/
* @date: 2008-05-13
* @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved.
* @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
* @website: http://www.datejs.com/
*/
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]};
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,p=function(s,l){if(!l){l=2;}
return("000"+s).slice(l*-1);};$P.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};$P.setTimeToNow=function(){var n=new Date();this.setHours(n.getHours());this.setMinutes(n.getMinutes());this.setSeconds(n.getSeconds());this.setMilliseconds(n.getMilliseconds());return this;};$D.today=function(){return new Date().clearTime();};$D.compare=function(date1,date2){if(isNaN(date1)||isNaN(date2)){throw new Error(date1+" - "+date2);}else if(date1 instanceof Date&&date2 instanceof Date){return(date1<date2)?-1:(date1>date2)?1:0;}else{throw new TypeError(date1+" - "+date2);}};$D.equals=function(date1,date2){return(date1.compareTo(date2)===0);};$D.getDayNumberFromName=function(name){var n=$C.dayNames,m=$C.abbreviatedDayNames,o=$C.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s||o[i].toLowerCase()==s){return i;}}
return-1;};$D.getMonthNumberFromName=function(name){var n=$C.monthNames,m=$C.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};$D.isLeapYear=function(year){return((year%4===0&&year%100!==0)||year%400===0);};$D.getDaysInMonth=function(year,month){return[31,($D.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};$D.getTimezoneAbbreviation=function(offset){var z=$C.timezones,p;for(var i=0;i<z.length;i++){if(z[i].offset===offset){return z[i].name;}}
return null;};$D.getTimezoneOffset=function(name){var z=$C.timezones,p;for(var i=0;i<z.length;i++){if(z[i].name===name.toUpperCase()){return z[i].offset;}}
return null;};$P.clone=function(){return new Date(this.getTime());};$P.compareTo=function(date){return Date.compare(this,date);};$P.equals=function(date){return Date.equals(this,date||new Date());};$P.between=function(start,end){return this.getTime()>=start.getTime()&&this.getTime()<=end.getTime();};$P.isAfter=function(date){return this.compareTo(date||new Date())===1;};$P.isBefore=function(date){return(this.compareTo(date||new Date())===-1);};$P.isToday=function(){return this.isSameDay(new Date());};$P.isSameDay=function(date){return this.clone().clearTime().equals(date.clone().clearTime());};$P.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};$P.addSeconds=function(value){return this.addMilliseconds(value*1000);};$P.addMinutes=function(value){return this.addMilliseconds(value*60000);};$P.addHours=function(value){return this.addMilliseconds(value*3600000);};$P.addDays=function(value){this.setDate(this.getDate()+value);return this;};$P.addWeeks=function(value){return this.addDays(value*7);};$P.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,$D.getDaysInMonth(this.getFullYear(),this.getMonth())));return this;};$P.addYears=function(value){return this.addMonths(value*12);};$P.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
var x=config;if(x.milliseconds){this.addMilliseconds(x.milliseconds);}
if(x.seconds){this.addSeconds(x.seconds);}
if(x.minutes){this.addMinutes(x.minutes);}
if(x.hours){this.addHours(x.hours);}
if(x.weeks){this.addWeeks(x.weeks);}
if(x.months){this.addMonths(x.months);}
if(x.years){this.addYears(x.years);}
if(x.days){this.addDays(x.days);}
return this;};var $y,$m,$d;$P.getWeek=function(){var a,b,c,d,e,f,g,n,s,w;$y=(!$y)?this.getFullYear():$y;$m=(!$m)?this.getMonth()+1:$m;$d=(!$d)?this.getDate():$d;if($m<=2){a=$y-1;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=0;f=$d-1+(31*($m-1));}else{a=$y;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=s+1;f=$d+((153*($m-3)+2)/5)+58+s;}
g=(a+b)%7;d=(f+g-e)%7;n=(f+3-d)|0;if(n<0){w=53-((g-s)/5|0);}else if(n>364+s){w=1;}else{w=(n/7|0)+1;}
$y=$m=$d=null;return w;};$P.getISOWeek=function(){$y=this.getUTCFullYear();$m=this.getUTCMonth()+1;$d=this.getUTCDate();return p(this.getWeek());};$P.setWeek=function(n){return this.moveToDayOfWeek(1).addWeeks(n-this.getWeek());};$D._validate=function(n,min,max,name){if(typeof n=="undefined"){return false;}else if(typeof n!="number"){throw new TypeError(n+" is not a Number.");}else if(n<min||n>max){throw new RangeError(n+" is not a valid value for "+name+".");}
return true;};$D.validateMillisecond=function(value){return $D._validate(value,0,999,"millisecond");};$D.validateSecond=function(value){return $D._validate(value,0,59,"second");};$D.validateMinute=function(value){return $D._validate(value,0,59,"minute");};$D.validateHour=function(value){return $D._validate(value,0,23,"hour");};$D.validateDay=function(value,year,month){return $D._validate(value,1,$D.getDaysInMonth(year,month),"day");};$D.validateMonth=function(value){return $D._validate(value,0,11,"month");};$D.validateYear=function(value){return $D._validate(value,0,9999,"year");};$P.set=function(config){if($D.validateMillisecond(config.millisecond)){this.addMilliseconds(config.millisecond-this.getMilliseconds());}
if($D.validateSecond(config.second)){this.addSeconds(config.second-this.getSeconds());}
if($D.validateMinute(config.minute)){this.addMinutes(config.minute-this.getMinutes());}
if($D.validateHour(config.hour)){this.addHours(config.hour-this.getHours());}
if($D.validateMonth(config.month)){this.addMonths(config.month-this.getMonth());}
if($D.validateYear(config.year)){this.addYears(config.year-this.getFullYear());}
if($D.validateDay(config.day,this.getFullYear(),this.getMonth())){this.addDays(config.day-this.getDate());}
if(config.timezone){this.setTimezone(config.timezone);}
if(config.timezoneOffset){this.setTimezoneOffset(config.timezoneOffset);}
if(config.week&&$D._validate(config.week,0,53,"week")){this.setWeek(config.week);}
return this;};$P.moveToFirstDayOfMonth=function(){return this.set({day:1});};$P.moveToLastDayOfMonth=function(){return this.set({day:$D.getDaysInMonth(this.getFullYear(),this.getMonth())});};$P.moveToNthOccurrence=function(dayOfWeek,occurrence){var shift=0;if(occurrence>0){shift=occurrence-1;}
else if(occurrence===-1){this.moveToLastDayOfMonth();if(this.getDay()!==dayOfWeek){this.moveToDayOfWeek(dayOfWeek,-1);}
return this;}
return this.moveToFirstDayOfMonth().addDays(-1).moveToDayOfWeek(dayOfWeek,+1).addWeeks(shift);};$P.moveToDayOfWeek=function(dayOfWeek,orient){var diff=(dayOfWeek-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};$P.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};$P.getOrdinalNumber=function(){return Math.ceil((this.clone().clearTime()-new Date(this.getFullYear(),0,1))/86400000)+1;};$P.getTimezone=function(){return $D.getTimezoneAbbreviation(this.getUTCOffset());};$P.setTimezoneOffset=function(offset){var here=this.getTimezoneOffset(),there=Number(offset)*-6/10;return this.addMinutes(there-here);};$P.setTimezone=function(offset){return this.setTimezoneOffset($D.getTimezoneOffset(offset));};$P.hasDaylightSavingTime=function(){return(Date.today().set({month:0,day:1}).getTimezoneOffset()!==Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.isDaylightSavingTime=function(){return(this.hasDaylightSavingTime()&&new Date().getTimezoneOffset()===Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r.charAt(0)+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};$P.getElapsed=function(date){return(date||new Date())-this;};if(!$P.toISOString){$P.toISOString=function(){function f(n){return n<10?'0'+n:n;}
return'"'+this.getUTCFullYear()+'-'+
f(this.getUTCMonth()+1)+'-'+
f(this.getUTCDate())+'T'+
f(this.getUTCHours())+':'+
f(this.getUTCMinutes())+':'+
f(this.getUTCSeconds())+'Z"';};}
$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}}
var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");}
x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}());
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());}
return false;};$P.at=function(time){return(typeof time==="string")?$D.parse(this.toString("d")+" "+time):this.set(time);};$N.fromNow=$N.after=function(date){var c={};c[this._dateElement]=this;return((!date)?new Date():date.clone()).add(c);};$N.ago=$N.before=function(date){var c={};c[this._dateElement]=this*-1;return((!date)?new Date():date.clone()).add(c);};var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),pxf=("Milliseconds Seconds Minutes Hours Date Week Month FullYear").split(/\s/),nth=("final first second third fourth fifth").split(/\s/),de;$P.toObject=function(){var o={};for(var i=0;i<px.length;i++){o[px[i].toLowerCase()]=this["get"+pxf[i]]();}
return o;};$D.fromObject=function(config){config.week=null;return Date.today().set(config);};var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
if(this._nth!==null){if(this._isSecond){this.addSeconds(this._orient*-1);}
this._isSecond=false;var ntemp=this._nth;this._nth=null;var temp=this.clone().moveToLastDayOfMonth();this.moveToNthOccurrence(n,ntemp);if(this>temp){throw new RangeError($D.getDayName(n)+" does not occur "+ntemp+" times in the month of "+$D.getMonthName(temp.getMonth())+" "+temp.getFullYear()+".");}
return this;}
return this.moveToDayOfWeek(n,this._orient);};};var sdf=function(n){return function(){var t=$D.today(),shift=n-t.getDay();if(n===0&&$C.firstDayOfWeek===1&&t.getDay()!==0){shift=shift+7;}
return t.addDays(shift);};};for(var i=0;i<dx.length;i++){$D[dx[i].toUpperCase()]=$D[dx[i].toUpperCase().substring(0,3)]=i;$D[dx[i]]=$D[dx[i].substring(0,3)]=sdf(i);$P[dx[i]]=$P[dx[i].substring(0,3)]=df(i);}
var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
return this.moveToMonth(n,this._orient);};};var smf=function(n){return function(){return $D.today().set({month:n,day:1});};};for(var j=0;j<mx.length;j++){$D[mx[j].toUpperCase()]=$D[mx[j].toUpperCase().substring(0,3)]=j;$D[mx[j]]=$D[mx[j].substring(0,3)]=smf(j);$P[mx[j]]=$P[mx[j].substring(0,3)]=mf(j);}
var ef=function(j){return function(){if(this._isSecond){this._isSecond=false;return this;}
if(this._same){this._same=this._is=false;var o1=this.toObject(),o2=(arguments[0]||new Date()).toObject(),v="",k=j.toLowerCase();for(var m=(px.length-1);m>-1;m--){v=px[m].toLowerCase();if(o1[v]!=o2[v]){return false;}
if(k==v){break;}}
return true;}
if(j.substring(j.length-1)!="s"){j+="s";}
return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$P[de]=$P[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}
$P._ss=ef("Second");var nthfn=function(n){return function(dayOfWeek){if(this._same){return this._ss(arguments[0]);}
if(dayOfWeek||dayOfWeek===0){return this.moveToNthOccurrence(dayOfWeek,n);}
this._nth=n;if(n===2&&(dayOfWeek===undefined||dayOfWeek===null)){this._isSecond=true;return this.addSeconds(this._orient);}
return this;};};for(var l=0;l<nth.length;l++){$P[nth[l]]=(l===0)?nthfn(-1):nthfn(l);}}());
(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
break;}
return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
rx.push(r[0]);s=r[1];}
return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){r=null;}
if(r){return r;}}
throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
rx.push(r[0]);s=r[1];}
return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
s=q[1];}
if(!r){throw new $P.Exception(s);}
if(q){throw new $P.Exception(q[1]);}
if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
if(!last&&q[1].length===0){last=true;}
if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
if(rx[1].length<best[1].length){best=rx;}
if(best[1].length===0){break;}}
if(best[0].length===0){return best;}
if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
best[1]=q[1];}
return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo;var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
return rx;};$D.Grammar={};$D.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=(s.length==3)?"jan feb mar apr may jun jul aug sep oct nov dec".indexOf(s)/4:Number(s)-1;};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<$C.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
var now=new Date();if((this.hour||this.minute)&&(!this.month&&!this.year&&!this.day)){this.day=now.getDate();}
if(!this.year){this.year=now.getFullYear();}
if(!this.month&&this.month!==0){this.month=now.getMonth();}
if(!this.day){this.day=1;}
if(!this.hour){this.hour=0;}
if(!this.minute){this.minute=0;}
if(!this.second){this.second=0;}
if(this.meridian&&this.hour){if(this.meridian=="p"&&this.hour<12){this.hour=this.hour+12;}else if(this.meridian=="a"&&this.hour==12){this.hour=0;}}
if(this.day>$D.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
var today=$D.today();if(this.now&&!this.unit&&!this.operator){return new Date();}else if(this.now){today=new Date();}
var expression=!!(this.days&&this.days!==null||this.orient||this.operator);var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(!this.now&&"hour minute second".indexOf(this.unit)!=-1){today.setTimeToNow();}
if(this.month||this.month===0){if("year day hour minute second".indexOf(this.unit)!=-1){this.value=this.month+1;this.month=null;expression=true;}}
if(!expression&&this.weekday&&!this.day&&!this.days){var temp=Date[this.weekday]();this.day=temp.getDate();if(!this.month){this.month=temp.getMonth();}
this.year=temp.getFullYear();}
if(expression&&this.weekday&&this.unit!="month"){this.unit="day";gap=($D.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
if(this.month&&this.unit=="day"&&this.operator){this.value=(this.month+1);this.month=null;}
if(this.value!=null&&this.month!=null&&this.year!=null){this.day=this.value*1;}
if(this.month&&!this.day&&this.value){today.set({day:this.value*1});if(!expression){this.day=this.value*1;}}
if(!this.month&&this.value&&this.unit=="month"&&!this.now){this.month=this.value;expression=true;}
if(expression&&(this.month||this.month===0)&&this.unit!="year"){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
if(!this.unit){this.unit="day";}
if(!this.value&&this.operator&&this.operator!==null&&this[this.unit+"s"]&&this[this.unit+"s"]!==null){this[this.unit+"s"]=this[this.unit+"s"]+((this.operator=="add")?1:-1)+(this.value||0)*orient;}else if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
this[this.unit+"s"]=this.value*orient;}
if(this.meridian&&this.hour){if(this.meridian=="p"&&this.hour<12){this.hour=this.hour+12;}else if(this.meridian=="a"&&this.hour==12){this.hour=0;}}
if(this.weekday&&!this.day&&!this.days){var temp=Date[this.weekday]();this.day=temp.getDate();if(temp.getMonth()!==today.getMonth()){this.month=temp.getMonth();}}
if((this.month||this.month===0)&&!this.day){this.day=1;}
if(!this.orient&&!this.operator&&this.unit=="week"&&this.value&&!this.day&&!this.month){return Date.today().setWeek(this.value);}
if(expression&&this.timezone&&this.day&&this.days){this.day=this.days;}
return(expression)?today.add(this):today.set(this);}};var _=$D.Parsing.Operators,g=$D.Grammar,t=$D.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|@|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=$C.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
fn=_C[keys]=_.any.apply(null,px);}
return fn;};g.ctoken2=function(key){return _.rtoken($C.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.m,g.s],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^((\+|\-)\s*\d\d\d\d)|((\+|\-)\d\d\:?\d\d)/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^((\+|\-)\s*\d\d\d\d)|((\+|\-)\d\d\:?\d\d)/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("second minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[$C.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw $D.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["\"yyyy-MM-ddTHH:mm:ssZ\"","yyyy-MM-ddTHH:mm:ssZ","yyyy-MM-ddTHH:mm:ssz","yyyy-MM-ddTHH:mm:ss","yyyy-MM-ddTHH:mmZ","yyyy-MM-ddTHH:mmz","yyyy-MM-ddTHH:mm","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","MMddyyyy","ddMMyyyy","Mddyyyy","ddMyyyy","Mdyyyy","dMyyyy","yyyy","Mdyy","dMyy","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
return g._start.call({},s);};$D._parse=$D.parse;$D.parse=function(s){var r=null;if(!s){return null;}
if(s instanceof Date){return s;}
try{r=$D.Grammar.start.call({},s.replace(/^\s*(\S*(\s+\S+)*)\s*$/,"$1"));}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};$D.getParseFunction=function(fx){var fn=$D.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};};$D.parseExact=function(s,fx){return $D.getParseFunction(fx)(s);};}());

View File

@ -0,0 +1,137 @@
/*
jQuery.ganttView v.0.8.0
Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com
MIT License Applies
*/
div.ganttview-hzheader-month,
div.ganttview-hzheader-day,
div.ganttview-vtheader,
div.ganttview-vtheader-item-name,
div.ganttview-vtheader-series,
div.ganttview-grid,
div.ganttview-grid-row-cell {
float: left;
}
div.ganttview-hzheader-month,
div.ganttview-hzheader-day {
text-align: center;
}
div.ganttview-grid-row-cell.last,
div.ganttview-hzheader-day.last,
div.ganttview-hzheader-month.last {
border-right: none;
}
div.ganttview {
border: 1px solid #999;
}
/* Horizontal Header */
div.ganttview-hzheader-month {
width: 60px;
height: 20px;
border-right: 1px solid #d0d0d0;
line-height: 20px;
}
div.ganttview-hzheader-day {
width: 20px;
height: 20px;
border-right: 1px solid #f0f0f0;
border-top: 1px solid #d0d0d0;
line-height: 20px;
color: #777;
}
/* Vertical Header */
div.ganttview-vtheader {
margin-top: 41px;
width: auto;
overflow: hidden;
background-color: #fff;
}
div.ganttview-vtheader-item {
overflow: hidden;
color: #666;
}
div.ganttview-vtheader-item-name {
width: 100px;
padding-left: 5px;
border-top: 1px solid #d0d0d0;
line-height: 16px;
}
div.ganttview-vtheader-series-name {
width: 130px;
height: 31px;
border-top: 1px solid #d0d0d0;
line-height: 16px;
padding-left: 5px;
}
/* Slider */
div.ganttview-slide-container {
overflow: auto;
border-left: 1px solid #999;
}
/* Grid */
div.ganttview-grid-row-cell {
width: 20px;
height: 31px;
border-right: 1px solid #f0f0f0;
border-top: 1px solid #f0f0f0;
}
div.ganttview-grid-row-cell.ganttview-weekend {
background-color: #fafafa;
}
div.ganttview-grid-row-cell.ganttview-today {
background-color: #F7F9FF !important;
}
/* Blocks */
div.ganttview-blocks {
margin-top: 40px;
}
div.ganttview-block-container {
height: 31px;
padding-top: 4px;
text-align: left;
}
div.ganttview-block {
position: relative;
height: 25px;
background-color: #E5ECF9;
border: 1px solid #c0c0c0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
div.ganttview-block-text {
position: absolute;
height: 12px;
font-size: 0.8em;
color: #999;
padding: 2px 3px;
}
/* Adjustments for jQuery UI Styling */
div.ganttview-block div.ui-resizable-handle.ui-resizable-s {
bottom: -0;
}

View File

@ -0,0 +1,396 @@
/*
jQuery.ganttView v.0.8.8
Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com
MIT License Applies
*/
/*
Options
-----------------
showWeekends: boolean
showToday: boolean
data: object
cellWidth: number
cellHeight: number
slideWidth: number
dataUrl: string
behavior: {
clickable: boolean,
draggable: boolean,
resizable: boolean,
onDblClick: function, // Modify by Flectra - Click becomes DblClick
onDrag: function,
onResize: function
}
*/
(function (jQuery) {
jQuery.fn.ganttView = function () {
var args = Array.prototype.slice.call(arguments);
if (args.length == 1 && typeof(args[0]) == "object") {
build.call(this, args[0]);
}
if (args.length == 2 && typeof(args[0]) == "string") {
handleMethod.call(this, args[0], args[1]);
}
};
function build(options) {
var els = this;
var defaults = {
showWeekends: true,
showToday: true,
cellWidth: 21,
cellHeight: 31,
slideWidth: 400,
vHeaderWidth: 100,
behavior: {
clickable: true,
draggable: true,
resizable: true
}
};
var opts = jQuery.extend(true, defaults, options);
if (opts.data) {
build();
} else if (opts.dataUrl) {
jQuery.getJSON(opts.dataUrl, function (data) { opts.data = data; build(); });
}
function build() {
var minDays = Math.floor((opts.slideWidth / opts.cellWidth) + 5);
var startEnd = DateUtils.getBoundaryDatesFromData(opts.data, minDays);
opts.start = startEnd[0];
opts.end = startEnd[1];
els.each(function () {
var container = jQuery(this);
var div = jQuery("<div>", { "class": "ganttview" });
new Chart(div, opts).render();
container.append(div);
var w = jQuery("div.ganttview-vtheader", container).outerWidth() +
jQuery("div.ganttview-slide-container", container).outerWidth();
container.css("width", (w + 2) + "px");
new Behavior(container, opts).apply();
});
}
}
function handleMethod(method, value) {
if (method == "setSlideWidth") {
var div = $("div.ganttview", this);
div.each(function () {
var vtWidth = $("div.ganttview-vtheader", div).outerWidth();
$(div).width(vtWidth + value + 1);
$("div.ganttview-slide-container", this).width(value);
});
}
}
var Chart = function(div, opts) {
function render() {
addVtHeader(div, opts.data, opts.cellHeight);
var slideDiv = jQuery("<div>", {
"class": "ganttview-slide-container",
"css": { "width": opts.slideWidth + "px" }
});
dates = getDates(opts.start, opts.end);
addHzHeader(slideDiv, dates, opts.cellWidth);
addGrid(slideDiv, opts.data, dates, opts.cellWidth, opts.showWeekends, opts.showToday);
addBlockContainers(slideDiv, opts.data);
addBlocks(slideDiv, opts.data, opts.cellWidth, opts.start);
div.append(slideDiv);
applyLastClass(div.parent());
}
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// Creates a 3 dimensional array [year][month][day] of every day
// between the given start and end dates
function getDates(start, end) {
var end = end.clone().addDays(6);
var dates = [];
dates[start.getFullYear()] = [];
dates[start.getFullYear()][start.getMonth()] = [start]
var last = start;
while (last.compareTo(end) == -1) {
var next = last.clone().addDays(1);
if (!dates[next.getFullYear()]) { dates[next.getFullYear()] = []; }
if (!dates[next.getFullYear()][next.getMonth()]) {
dates[next.getFullYear()][next.getMonth()] = [];
}
dates[next.getFullYear()][next.getMonth()].push(next);
last = next;
}
return dates;
}
function addVtHeader(div, data, cellHeight) {
var headerDiv = jQuery("<div>", { "class": "ganttview-vtheader" });
for (var i = 0; i < data.length; i++) {
var itemDiv = jQuery("<div>", { "class": "ganttview-vtheader-item" });
if ($.trim(data[i].name).length > 0)
itemDiv.append(jQuery("<div>", {
"class": "ganttview-vtheader-item-name",
"css": { "height": (data[i].series.length * cellHeight) + "px" }
}).append(data[i].name));
var seriesDiv = jQuery("<div>", { "class": "ganttview-vtheader-series" });
for (var j = 0; j < data[i].series.length; j++) {
seriesDiv.append(jQuery("<div>", { "class": "ganttview-vtheader-series-name" })
.append(data[i].series[j].name));
}
itemDiv.append(seriesDiv);
headerDiv.append(itemDiv);
}
div.append(headerDiv);
}
function addHzHeader(div, dates, cellWidth) {
var headerDiv = jQuery("<div>", { "class": "ganttview-hzheader" });
var monthsDiv = jQuery("<div>", { "class": "ganttview-hzheader-months" });
var daysDiv = jQuery("<div>", { "class": "ganttview-hzheader-days" });
var totalW = 0;
for (var y in dates) {
for (var m in dates[y]) {
var w = dates[y][m].length * cellWidth;
totalW = totalW + w;
monthsDiv.append(jQuery("<div>", {
"class": "ganttview-hzheader-month",
"css": { "width": w + "px" }
}).append(monthNames[m] + "/" + y));
for (var d in dates[y][m]) {
daysDiv.append(jQuery("<div>", { "class": "ganttview-hzheader-day" })
.append(dates[y][m][d].getDate()));
}
}
}
monthsDiv.css("width", totalW + "px");
daysDiv.css("width", totalW + "px");
headerDiv.append(monthsDiv).append(daysDiv);
div.append(headerDiv);
}
function addGrid(div, data, dates, cellWidth, showWeekends, showToday) {
var gridDiv = jQuery("<div>", { "class": "ganttview-grid" });
var rowDiv = jQuery("<div>", { "class": "ganttview-grid-row" });
for (var y in dates) {
for (var m in dates[y]) {
for (var d in dates[y][m]) {
var cellDiv = jQuery("<div>", { "class": "ganttview-grid-row-cell" });
if (DateUtils.isWeekend(dates[y][m][d]) && showWeekends) {
cellDiv.addClass("ganttview-weekend");
}
if (DateUtils.isToday(dates[y][m][d]) && showToday) {
cellDiv.addClass("ganttview-today");
}
rowDiv.append(cellDiv);
}
}
}
var w = jQuery("div.ganttview-grid-row-cell", rowDiv).length * cellWidth;
rowDiv.css("width", w + "px");
gridDiv.css("width", w + "px");
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].series.length; j++) {
gridDiv.append(rowDiv.clone());
}
}
div.append(gridDiv);
}
function addBlockContainers(div, data) {
var blocksDiv = jQuery("<div>", { "class": "ganttview-blocks" });
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].series.length; j++) {
blocksDiv.append(jQuery("<div>", { "class": "ganttview-block-container" }));
}
}
div.append(blocksDiv);
}
function addBlocks(div, data, cellWidth, start) {
var rows = jQuery("div.ganttview-blocks div.ganttview-block-container", div);
var rowIdx = 0;
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].series.length; j++) {
var series = data[i].series[j];
var size = DateUtils.daysBetween(series.start, series.end) + 1;
var offset = DateUtils.daysBetween(start, series.start);
var block = jQuery("<div>", {
"class": "ganttview-block",
"title": series.name + ", " + size + " days \n"+ series.start +" to "+ series.end,
"css": {
"width": ((size * cellWidth) - 7) + "px",
"margin-left": ((offset * cellWidth) + 3) + "px"
}
});
addBlockData(block, data[i], series);
if (data[i].series[j].color) {
block.css("background-color", data[i].series[j].color);
}
block.append(jQuery("<div>", { "class": "ganttview-block-text" }).text(size));
jQuery(rows[rowIdx]).append(block);
rowIdx = rowIdx + 1;
}
}
}
function addBlockData(block, data, series) {
// This allows custom attributes to be added to the series data objects
// and makes them available to the 'data' argument of click, resize, and drag handlers
var blockData = { id: data.id, name: data.name };
jQuery.extend(blockData, series);
block.data("block-data", blockData);
}
function applyLastClass(div) {
jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child", div).addClass("last");
jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child", div).addClass("last");
jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child", div).addClass("last");
}
return {
render: render
};
}
var Behavior = function (div, opts) {
function apply() {
if (opts.behavior.clickable) {
bindBlockClick(div, opts.behavior.onDblClick);
}
if (opts.behavior.resizable) {
bindBlockResize(div, opts.cellWidth, opts.start, opts.behavior.onResize);
}
if (opts.behavior.draggable) {
bindBlockDrag(div, opts.cellWidth, opts.start, opts.behavior.onDrag);
}
}
function bindBlockClick(div, callback) {
jQuery("div.ganttview-block", div).on("dblclick", function () {
if (callback) { callback(jQuery(this).data("block-data")); }
});
}
function bindBlockResize(div, cellWidth, startDate, callback) {
jQuery("div.ganttview-block", div).resizable({
grid: cellWidth,
handles: "e,w",
stop: function () {
var block = jQuery(this);
updateDataAndPosition(div, block, cellWidth, startDate);
if (callback) { callback(block.data("block-data")); }
}
});
}
function bindBlockDrag(div, cellWidth, startDate, callback) {
jQuery("div.ganttview-block", div).draggable({
axis: "x",
grid: [cellWidth, cellWidth],
stop: function () {
var block = jQuery(this);
updateDataAndPosition(div, block, cellWidth, startDate);
if (callback) { callback(block.data("block-data")); }
}
});
}
function updateDataAndPosition(div, block, cellWidth, startDate) {
var container = jQuery("div.ganttview-slide-container", div);
var scroll = container.scrollLeft();
var offset = block.offset().left - container.offset().left - 1 + scroll;
// Set new start date
var daysFromStart = Math.round(offset / cellWidth);
var newStart = startDate.clone().addDays(daysFromStart);
block.data("block-data").start = newStart;
// Set new end date
var width = block.outerWidth();
var numberOfDays = Math.round(width / cellWidth) - 1;
block.data("block-data").end = newStart.clone().addDays(numberOfDays);
jQuery("div.ganttview-block-text", block).text(numberOfDays + 1);
// Remove top and left properties to avoid incorrect block positioning,
// set position to relative to keep blocks relative to scrollbar when scrolling
block.css("top", "").css("left", "")
.css("position", "relative").css("margin-left", offset + "px");
}
return {
apply: apply
};
}
var ArrayUtils = {
contains: function (arr, obj) {
var has = false;
for (var i = 0; i < arr.length; i++) { if (arr[i] == obj) { has = true; } }
return has;
}
};
var DateUtils = {
daysBetween: function (start, end) {
if (!start || !end) { return 0; }
start = Date.parse(start); end = Date.parse(end);
if (start.getYear() == 1901 || end.getYear() == 8099) { return 0; }
var count = 0, date = start.clone();
while (date.compareTo(end) == -1) { count = count + 1; date.addDays(1); }
return count;
},
isWeekend: function (date) {
return date.getDay() % 6 == 0;
},
isToday: function (date) {
return date.isToday();
},
getBoundaryDatesFromData: function (data, minDays) {
var minStart = new Date(); maxEnd = new Date();
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].series.length; j++) {
var start = Date.parse(data[i].series[j].start);
var end = Date.parse(data[i].series[j].end)
if (i == 0 && j == 0) { minStart = start; maxEnd = end; }
if (minStart.compareTo(start) == 1) { minStart = start; }
if (maxEnd.compareTo(end) == -1) { maxEnd = end; }
}
}
// Insure that the width of the chart is at least the slide width to avoid empty
// whitespace to the right of the grid
if (DateUtils.daysBetween(minStart, maxEnd) < minDays) {
maxEnd = minStart.clone().addDays(minDays);
}
return [minStart, maxEnd];
}
};
})(jQuery);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,25 @@
flectra.define('web.GanttController', function (require) {
"use strict";
/*---------------------------------------------------------
* Flectra Gantt view
*---------------------------------------------------------*/
var AbstractController = require('web.AbstractController');
return AbstractController.extend({
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
updateRecord: '_onUpdateRecord',
}),
_onUpdateRecord: function (record) {
this._rpc({
model: this.model.modelName,
method: 'write',
args: [record.data.id, {
[this.model.data.arch['date_start']]: record.data.start.toString('yyyy-M-d'),
[this.model.data.arch['date_stop']]: record.data.end.toString('yyyy-M-d'),
}],
}).then(this.reload.bind(this));
},
});
});

View File

@ -0,0 +1,144 @@
flectra.define('web.GanttModel', function (require) {
"use strict";
/**
* The gantt model is responsible for fetching and processing data from the
* server. It basically just do a search_read and format/normalize data.
*/
var AbstractModel = require('web.AbstractModel');
return AbstractModel.extend({
/**
* @override
* @param {Object} params
*/
init: function () {
this._super.apply(this, arguments);
this.data = null;
},
/**
* @override
* @param {Object} params
* @param {string[]} params.groupedBy a list of valid field names
* @param {Object} params.context
* @param {string[]} params.domain
* @returns {Deferred}
*/
load: function (params) {
this.modelName = params.modelName;
this.data = {
records: [],
domain: params.domain,
context: params.context,
groupedBy: params.groupedBy || [],
arch: params.arch.attrs,
};
return this._loadData();
},
/**
* @override
* @param {Object} params
* @param {string[]} params.groupedBy a list of valid field names
* @param {Object} params.context
* @param {string[]} params.domain
* @returns {Deferred}
*/
reload: function (handle, params) {
if (params.domain) {
this.data.domain = params.domain;
}
if (params.context) {
this.data.context = params.context;
}
if (params.groupBy) {
this.data.groupedBy = params.groupBy;
}
return this._loadData();
},
/**
* @returns {Deferred}
*/
_loadData: function () {
var self = this;
return this._rpc({
model: this.modelName,
method: 'search_read',
context: this.data.context,
domain: this.data.domain,
})
.then(function (records) {
self.data.records = self._processData(records);
});
},
_processData: function (raw_datas) {
/**
* GroupBy is only supported till 1st level !
* @todo Flectra: Support Multi level GroupBy
*/
var self = this;
var ganttData = [];
if (self.data.groupedBy.length) {
_.each(raw_datas, function (raw_data) {
var grpByStr = raw_data[self.data.groupedBy[0]] ? raw_data[self.data.groupedBy[0]] : 'Undefined';
if (grpByStr && grpByStr instanceof Array) {
grpByStr = raw_data[self.data.groupedBy[0]] ? raw_data[self.data.groupedBy[0]][1] : 'Undefined';
}
var keyCheck = _.findKey(ganttData, {name: grpByStr});
if (!keyCheck) {
ganttData.push({
name: grpByStr,
series: [],
});
}
keyCheck = _.findKey(ganttData, {name: grpByStr});
if (ganttData[keyCheck]) {
if (raw_data[self.data.arch['date_stop']]) {
ganttData[keyCheck].series.push({
id: raw_data['id'],
name: raw_data['display_name'],
start: raw_data[self.data.arch['date_start']].split(' ')[0],
end: raw_data[self.data.arch['date_stop']].split(' ')[0]
});
} else {
ganttData[keyCheck].series.push({
id: raw_data['id'],
name: raw_data['display_name'],
start: raw_data[self.data.arch['date_start']].split(' ')[0],
end: raw_data[self.data.arch['date_start']].split(' ')[0]
});
}
}
});
} else {
_.each(raw_datas, function (raw_data) {
if (raw_data[self.data.arch['date_stop']]) {
ganttData.push({
series: [
{
id: raw_data['id'],
name: raw_data['display_name'],
start: raw_data[self.data.arch['date_start']].split(' ')[0],
end: raw_data[self.data.arch['date_stop']].split(' ')[0]
},
],
});
} else {
ganttData.push({
series: [
{
id: raw_data['id'],
name: raw_data['display_name'],
start: raw_data[self.data.arch['date_start']].split(' ')[0],
end: raw_data[self.data.arch['date_start']].split(' ')[0]
},
],
});
}
});
}
return ganttData;
},
});
});

View File

@ -0,0 +1,122 @@
flectra.define('web.GanttRenderer', function (require) {
"use strict";
/**
* The graph renderer turns the data from the graph model into a nice looking
* svg chart. This code uses the nvd3 library.
*
* Note that we use a custom build for the nvd3, with only the model we actually
* use.
*/
var core = require('web.core');
var AbstractRenderer = require('web.AbstractRenderer');
var Dialog = require('web.Dialog');
var _t = core._t;
var QWeb = core.qweb;
return AbstractRenderer.extend({
template: "GanttView",
/**
* @override
* @param {Widget} parent
* @param {Object} state
* @param {Object} params
* @param {boolean} params.stacked
*/
init: function (parent, state, params) {
this.parent = parent;
this._super.apply(this, arguments);
},
/**
* Render the chart.
*
* Note that This method is synchronous, but the actual rendering is done
* asynchronously (in a setTimeout). The reason for that is that nvd3/d3
* needs to be in the DOM to correctly render itself. So, we trick Flectra by
* returning immediately, then wait a tiny interval before actually
* displaying the data.
*
* @returns {Deferred} The _super deferred is actually resolved immediately
*/
_render: function () {
this.data = this.parent.active_view.controller.model.data;
this._loadGanttView();
return $.when();
},
_loadGanttView: function () {
var self = this;
this.$el.empty().ganttView({
data: self.data.records,
slideWidth: 'auto',
cellWidth: 20,
behavior: {
onDblClick: function (data) {
var dialog = new Dialog(self, {
title: _t(data.name),
$content: $(QWeb.render('GanttViewWizard')),
size: 'small',
buttons: [
{text: _t("Save"), classes: 'btn-success', click: _.bind(_callSave, self)},
{text: _t("Cancel"), classes: 'btn-danger', close: true}
]
}).open();
dialog.opened().then(function () {
var datepickers_options = {
keepOpen: true,
minDate: moment({y: 1900}),
maxDate: moment().add(200, "y"),
calendarWeeks: true,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
next: 'fa fa-chevron-right',
previous: 'fa fa-chevron-left',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
},
locale: moment.locale(),
format: "YYYY-MM-DD",
ignoreReadonly: true
};
dialog.$el.find('input#start_date').val(data.start);
dialog.$el.find('input#end_date').val(data.end);
dialog.$el.find('input#start_date, input#end_date').datetimepicker(datepickers_options);
});
function _callSave(event) {
var newData = {
start: dialog.$el.find('input#start_date').val().toString(),
end: dialog.$el.find('input#end_date').val().toString(),
id: data.id
};
if (data.start !== newData.start || data.end !== newData.end) {
var start_data = new Date(dialog.$el.find('input#start_date').val().toString()).getTime();
var end_data = new Date(dialog.$el.find('input#end_date').val().toString()).getTime();
if(start_data <= end_data){
self.trigger_up('updateRecord', newData);
dialog.close();
}else {
self.do_warn(_t("Warning"), _t("Start date should be less than or equal to End date"));
}
}
}
},
onResize: function (data) {
self.trigger_up('updateRecord', data);
},
onDrag: function (data) {
self.trigger_up('updateRecord', data);
},
}
});
this.$el.removeAttr('style');
},
});
});

View File

@ -0,0 +1,32 @@
flectra.define('web.GanttView', function (require) {
"use strict";
var AbstractView = require('web.AbstractView');
var core = require('web.core');
var GanttModel = require('web.GanttModel');
var GanttRenderer = require('web.GanttRenderer');
var Controller = require('web.GanttController');
var _lt = core._lt;
var GanttView = AbstractView.extend({
display_name: _lt('Gantt'),
icon: 'fa-tasks',
config: {
Model: GanttModel,
Controller: Controller,
Renderer: GanttRenderer,
},
/**
* @override
*/
init: function(viewInfo) {
this._super.apply(this, arguments);
var arch = viewInfo.arch;
this.loadParams.arch = arch;
},
});
return GanttView;
});

View File

@ -31,6 +31,7 @@ var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var PivotView = require('web.PivotView');
var CalendarView = require('web.CalendarView');
var GanttView = require('web.GanttView');
var view_registry = require('web.view_registry');
view_registry
@ -39,6 +40,7 @@ view_registry
.add('kanban', KanbanView)
.add('graph', GraphView)
.add('pivot', PivotView)
.add('calendar', CalendarView);
.add('calendar', CalendarView)
.add('gantt', GanttView);
});

View File

@ -528,6 +528,39 @@
<p><t t-esc="description"/></p>
</div>
<t t-name="GanttView">
<div class="o_gantt_view_container"/>
</t>
<t t-name="GanttViewWizard">
<table class="o_group o_inner_group" style="width: 100%;">
<tbody>
<tr>
<td class="o_td_label" style="padding: 10px 10px 10px 20px;">
<label class="o_form_label" for="start_date">Start Date</label>
</td>
<td style="width: 65%; padding: 10px 10px 10px 20px;">
<div class="o_datepicker o_field_date o_field_widget" name="date_from">
<input class="o_datepicker_input o_input"
type="text" name="date_from" id="start_date"/>
</div>
</td>
</tr>
<tr>
<td class="o_td_label" style="padding: 10px 10px 10px 20px;">
<label class="o_form_label" for="end_date">End Date</label>
</td>
<td style="width: 65%; padding: 10px 10px 10px 20px;">
<div class="o_datepicker o_field_date o_field_widget" name="date_to">
<input class="o_datepicker_input o_input"
type="text" name="date_to" id="end_date"/>
</div>
</td>
</tr>
</tbody>
</table>
</t>
<div t-name="PivotView" class="o_pivot">
<div class="o_field_selection"/>
</div>
@ -1332,7 +1365,6 @@
<li><a href="#" data-menu="support">Support</a></li>
<li class="divider"/>
<li><a href="#" data-menu="settings">Preferences</a></li>
<li><a href="#" data-menu="account">My Flectra.com account</a></li>
<li><a href="#" data-menu="logout">Log out</a></li>
</t>

View File

@ -236,6 +236,18 @@
<script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
<script type="text/javascript" src="/web/static/src/js/report/client_action.js"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/report_backend.less"/>
<!-- @Flectra: Gantt View Assets ::: Start -->
<script type="text/javascript" src="/web/static/lib/jquery.ganttView/date.js"/>
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ganttView/jquery.ganttView.css"/>
<script type="text/javascript" src="/web/static/lib/jquery.ganttView/jquery.ganttView.js"/>
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_model.js"/>
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_controller.js"/>
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_renderer.js"/>
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_view.js"/>
<!-- @Flectra: Gantt View Assets ::: End -->
</template>
<template id="web.assets_frontend" name="Website Assets">
@ -370,9 +382,6 @@
</div>
</t>
</div>
<div class="o_sub_menu_footer">
Powered by <a href="http://www.flectra.com" target="_blank"><span>Flectra</span></a>
</div>
</template>
<template id="web.menu_secondary_submenu">
<ul t-if="menu['children']" class="oe_secondary_submenu nav nav-pills nav-stacked">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -160,7 +160,7 @@
<div>
<div class="tab-content">
<div id="settings" class="tab-pane active text-muted text-center o_web_settings_dashboard_compact_subtitle">
<small>Copyright © 2004-2016 <a target="_blank" href="https://www.flectra.com" style="text-decoration: underline;">Flectra S.A.</a> <a target="_blank" href="http://www.gnu.org/licenses/lgpl.html" style="text-decoration: underline;">GNU LGPL Licensed</a></small>
<small>Copyright © 2004-Today <a target="_blank" href="https://www.flectrahq.com" style="text-decoration: underline;"> FlectraHQ</a> , <a href="https://www.odoo.com" style="text-decoration: underline;" target="_blank">Odoo S.A.</a> , <a target="_blank" href="http://www.gnu.org/licenses/lgpl.html" style="text-decoration: underline;">GNU LGPL Licensed</a></small>
</div>
</div>
</div>

View File

@ -20,7 +20,6 @@
<div class="mt32">
<address>
For the Flectra Team,<br/>
Fabien Pinckaers, Founder
</address>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Some files were not shown because too many files have changed in this diff Show More