summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-06-15 18:39:59 +0200
committerChristophe Simonis <chs@odoo.com>2017-06-15 18:39:59 +0200
commit9713dc2e1f515b1260eaf26ec6bbfd5443f37d65 (patch)
tree18250e927df2a339d616206b99d23edf0d13c6b3 /addons
parent6c94264f99333fda0337169b8a51ef8b30edebf9 (diff)
parent96595772e0859cc272bc022ab8c22852ba3d06f3 (diff)
[MERGE] forward port branch saas-14 up to 96595772e08
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_move.py3
-rw-r--r--addons/account_asset/models/account_asset.py2
-rw-r--r--addons/anonymization/wizard/anonymize_wizard.py8
-rw-r--r--addons/auth_oauth/models/res_users.py2
-rw-r--r--addons/hw_screen/__manifest__.py (renamed from addons/hw_screen/__openerp__.py)0
-rw-r--r--addons/l10n_fr_certification/__init__.py33
-rw-r--r--addons/l10n_fr_certification/__manifest__.py19
-rw-r--r--addons/l10n_fr_certification/data/account_move.xml13
-rw-r--r--addons/l10n_fr_certification/models/__init__.py2
-rw-r--r--addons/l10n_fr_certification/models/account.py155
-rw-r--r--addons/l10n_fr_certification/models/res_company.py42
-rw-r--r--addons/l10n_fr_certification/views/no_cancel.xml36
-rw-r--r--addons/l10n_in_schedule6/__manifest__.py (renamed from addons/l10n_in_schedule6/__openerp__.py)0
-rw-r--r--addons/mail/models/mail_template.py16
-rw-r--r--addons/point_of_sale/models/pos_order.py17
-rw-r--r--addons/point_of_sale/models/pos_session.py12
-rwxr-xr-xaddons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart9
-rw-r--r--addons/point_of_sale/views/pos_order_view.xml4
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js5
-rw-r--r--addons/resource/tests/test_resource.py4
-rw-r--r--addons/sale/models/sale.py11
-rw-r--r--addons/sale_margin/models/sale_order.py2
-rw-r--r--addons/sale_margin/tests/test_sale_margin.py26
-rw-r--r--addons/web_kanban/static/src/less/kanban_dashboard.less5
-rw-r--r--addons/website/models/ir_http.py4
-rw-r--r--addons/website_sale_options/static/src/js/website_sale.js5
26 files changed, 401 insertions, 34 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index d87e3eeeea5..4a4f1d39133 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1331,9 +1331,8 @@ class AccountMoveLine(models.Model):
""" Create analytic items upon validation of an account.move.line having an analytic account. This
method first remove any existing analytic item related to the line before creating any new one.
"""
+ self.mapped('analytic_line_ids').unlink()
for obj_line in self:
- if obj_line.analytic_line_ids:
- obj_line.analytic_line_ids.unlink()
if obj_line.analytic_account_id:
vals_line = obj_line._prepare_analytic_line()[0]
self.env['account.analytic.line'].create(vals_line)
diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py
index 83639b5f3a5..d6bc83b9087 100644
--- a/addons/account_asset/models/account_asset.py
+++ b/addons/account_asset/models/account_asset.py
@@ -477,7 +477,7 @@ class AccountAssetDepreciationLine(models.Model):
depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self)
company_currency = line.asset_id.company_id.currency_id
current_currency = line.asset_id.currency_id
- amount = current_currency.compute(line.amount, company_currency)
+ amount = current_currency.with_context(date=depreciation_date).compute(line.amount, company_currency)
asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids))
move_line_1 = {
'name': asset_name,
diff --git a/addons/anonymization/wizard/anonymize_wizard.py b/addons/anonymization/wizard/anonymize_wizard.py
index a0b355d4330..0d0ecfaaa63 100644
--- a/addons/anonymization/wizard/anonymize_wizard.py
+++ b/addons/anonymization/wizard/anonymize_wizard.py
@@ -4,16 +4,14 @@
import base64
import os
import random
-try:
- import cPickle as pickle
-except ImportError:
- import pickle
+
from lxml import etree
from operator import itemgetter
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.release import version_info
+from odoo.tools import pickle
from odoo.tools.safe_eval import safe_eval
from odoo.addons.anonymization.models.anonymization import group
@@ -207,7 +205,7 @@ class IrModelFieldsAnonymizeWizard(models.TransientModel):
# save pickle:
fn = open(abs_filepath, 'w')
- pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL)
+ pickle.dump(data, fn, protocol=-1)
# update the anonymization fields:
ano_fields.write({'state': 'anonymized'})
diff --git a/addons/auth_oauth/models/res_users.py b/addons/auth_oauth/models/res_users.py
index f48802c2310..b10f19a28be 100644
--- a/addons/auth_oauth/models/res_users.py
+++ b/addons/auth_oauth/models/res_users.py
@@ -10,6 +10,8 @@ from odoo import api, fields, models
from odoo.exceptions import AccessDenied
from odoo.addons.auth_signup.models.res_users import SignupError
+from odoo.addons import base
+base.res.res_users.USER_PRIVATE_FIELDS.append('oauth_access_token')
class ResUsers(models.Model):
_inherit = 'res.users'
diff --git a/addons/hw_screen/__openerp__.py b/addons/hw_screen/__manifest__.py
index 8ed5711b836..8ed5711b836 100644
--- a/addons/hw_screen/__openerp__.py
+++ b/addons/hw_screen/__manifest__.py
diff --git a/addons/l10n_fr_certification/__init__.py b/addons/l10n_fr_certification/__init__.py
new file mode 100644
index 00000000000..24644871f1e
--- /dev/null
+++ b/addons/l10n_fr_certification/__init__.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import models
+from openerp import api, SUPERUSER_ID
+
+def _setup_inalterability(cr, registry):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ # enable ping for this module
+ env['publisher_warranty.contract'].update_notification(cron_mode=True)
+
+ # make sure account_cancel is not usable at the same time as l10n_fr
+ # FORWARD PORT NOTICE
+ # In master as of March 2017, RCO-ODOO coded an exclusive field on modules to flag incompatibility
+
+ wanted_states = ['installed', 'to upgrade', 'to install']
+ account_cancel_module = env['ir.module.module'].search([('name', '=', 'account_cancel')], limit=1)
+
+ if account_cancel_module and account_cancel_module.state in wanted_states:
+ views_xml_id = env['ir.model.data'].search([('module', '=', 'account_cancel'), ('model', '=', 'ir.ui.view')])
+ ir_views = env['ir.ui.view'].browse([v.res_id for v in views_xml_id])
+ for cancel_view in ir_views:
+ cancel_view.write({'active': False})
+
+ fr_companies = env['res.company'].search([('partner_id.country_id.code', '=', 'FR')])
+ if fr_companies:
+ # create the securisation sequence per company
+ fr_companies._create_secure_sequence()
+
+ #reset the update_posted field on journals
+ journals = env['account.journal'].search([('company_id', 'in', fr_companies.ids)])
+ for journal in journals:
+ journal.write({'update_posted': False})
diff --git a/addons/l10n_fr_certification/__manifest__.py b/addons/l10n_fr_certification/__manifest__.py
new file mode 100644
index 00000000000..e83e9102914
--- /dev/null
+++ b/addons/l10n_fr_certification/__manifest__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'France - Accounting - Certified CGI 286 I-3 bis',
+ 'version': '1.0',
+ 'category': 'Localization',
+ 'description': """
+""",
+ 'depends': ['l10n_fr'],
+ 'installable': True,
+ 'auto_install': True,
+ 'application': True,
+ 'data': [
+ 'views/no_cancel.xml',
+ 'data/account_move.xml',
+ ],
+ 'post_init_hook': '_setup_inalterability',
+}
diff --git a/addons/l10n_fr_certification/data/account_move.xml b/addons/l10n_fr_certification/data/account_move.xml
new file mode 100644
index 00000000000..95adc8d8c2e
--- /dev/null
+++ b/addons/l10n_fr_certification/data/account_move.xml
@@ -0,0 +1,13 @@
+<odoo>
+ <record model="ir.actions.server" id="action_check_hash_integrity">
+ <field name="name">Intégrité des données comptables</field>
+ <field name="model_id" ref="account.model_account_move"/>
+ <field name="type">ir.actions.server</field>
+ <field name="code">
+ action = env['account.move']._check_hash_integrity(env.user.company_id.id)
+ </field>
+ </record>
+
+ <menuitem action="action_check_hash_integrity" id="menu_check_move_integrity" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
+
+</odoo>
diff --git a/addons/l10n_fr_certification/models/__init__.py b/addons/l10n_fr_certification/models/__init__.py
new file mode 100644
index 00000000000..17a7d5cf526
--- /dev/null
+++ b/addons/l10n_fr_certification/models/__init__.py
@@ -0,0 +1,2 @@
+import res_company
+import account
diff --git a/addons/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py
new file mode 100644
index 00000000000..ecb0f5aff8b
--- /dev/null
+++ b/addons/l10n_fr_certification/models/account.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+from hashlib import sha256
+from json import dumps
+
+from openerp import models, api, fields
+from openerp.tools.translate import _
+from openerp.exceptions import UserError
+
+ERR_MSG = _("According to the french law, you cannot modify a %s in order for its posted data to be updated or deleted. Field: %s")
+
+#forbidden fields
+MOVE_FIELDS = ['date', 'journal_id', 'company_id']
+LINE_FIELDS = ['debit', 'credit', 'account_id', 'move_id', 'partner_id']
+
+
+class AccountMove(models.Model):
+ _inherit = "account.move"
+
+ l10n_fr_secure_sequence_number = fields.Integer(readonly=True, copy=False)
+ l10n_fr_hash = fields.Char(readonly=True, copy=False)
+ l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False)
+
+ def _get_new_hash(self, secure_seq_number):
+ """ Returns the hash to write on journal entries when they get posted"""
+ self.ensure_one()
+ #get the only one exact previous move in the securisation sequence
+ prev_move = self.search([('state', '=', 'posted'),
+ ('company_id', '=', self.company_id.id),
+ ('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)])
+ if prev_move and len(prev_move) != 1:
+ raise UserError(
+ _('Error occured when computing the hash. Impossible to get the unique previous posted move'))
+
+ #build and return the hash
+ return self._compute_hash(prev_move.l10n_fr_hash if prev_move else '')
+
+ def _compute_hash(self, previous_hash):
+ """ Computes the hash of the browse_record given as self, based on the hash
+ of the previous record in the company's securisation sequence given as parameter"""
+ self.ensure_one()
+ hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
+ return hash_string.hexdigest()
+
+ def _compute_string_to_hash(self):
+ def _getattrstring(obj, field_str):
+ field_value = obj[field_str]
+ if obj._fields[field_str].type == 'many2one':
+ field_value = field_value.id
+ return str(field_value)
+
+ for move in self:
+ values = {}
+ for field in MOVE_FIELDS:
+ values[field] = _getattrstring(move, field)
+
+ for line in move.line_ids:
+ for field in LINE_FIELDS:
+ values[field] = _getattrstring(line, field)
+ #make the json serialization canonical
+ # (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
+ move.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
+ ensure_ascii=True, indent=None,
+ separators=(',',':'))
+
+ @api.multi
+ def write(self, vals):
+ has_been_posted = False
+ for move in self:
+ if move.company_id.country_id.code == 'FR':
+ # write the hash and the secure_sequence_number when posting an account.move
+ if vals.get('state') == 'posted':
+ has_been_posted = True
+
+ # restrict the operation in case we are trying to write a forbidden field
+ if (move.state == "posted" and set(vals).intersection(MOVE_FIELDS)):
+ raise UserError(ERR_MSG % (self._name, ', '.join(MOVE_FIELDS)))
+ # restrict the operation in case we are trying to overwrite existing hash
+ if (move.l10n_fr_hash and 'l10n_fr_hash' in vals) or (move.l10n_fr_secure_sequence_number and 'l10n_fr_secure_sequence_number' in vals):
+ raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.'))
+ res = super(AccountMove, self).write(vals)
+ # write the hash and the secure_sequence_number when posting an account.move
+ if has_been_posted:
+ for move in self.filtered(lambda m: m.company_id.country_id.code == 'FR' and
+ not (m.l10n_fr_secure_sequence_number or m.l10n_fr_hash)):
+ new_number = move.company_id.l10n_fr_secure_sequence_id.next_by_id()
+ vals_hashing = {'l10n_fr_secure_sequence_number': new_number,
+ 'l10n_fr_hash': move._get_new_hash(new_number)}
+ res |= super(AccountMove, move).write(vals_hashing)
+ return res
+
+ def button_cancel(self):
+ #by-pass the normal behavior/message that tells people can cancel a posted journal entry
+ #if the journal allows it.
+ if self.company_id.country_id.code == 'FR':
+ raise UserError(_('You cannot modify a posted entry of a journal.'))
+ super(AccountMove, self).button_cancel()
+
+ @api.model
+ def _check_hash_integrity(self, company_id):
+ """Checks that all posted moves have still the same data as when they were posted
+ and raises an error with the result.
+ """
+ moves = self.search([('state', '=', 'posted'),
+ ('company_id', '=', company_id),
+ ('l10n_fr_secure_sequence_number', '!=', False)],
+ order="l10n_fr_secure_sequence_number ASC")
+
+ if not moves:
+ raise UserError(_('Warning: impossible to find any move with a hash.'))
+ previous_hash = ''
+ start_move_info = []
+ for move in moves:
+ if move.l10n_fr_hash != move._compute_hash(previous_hash=previous_hash):
+ raise UserError(_('Corrupted Data on move %s.') % move.id)
+ if not previous_hash:
+ #save the date and sequence number of the first move hashed
+ start_move_info = [move.date, move.l10n_fr_secure_sequence_number]
+ previous_hash = move.l10n_fr_hash
+ end_move_info = [move.date, move.l10n_fr_secure_sequence_number]
+ raise UserError(_('''Successfully checked the integrity of account moves.
+
+ The account moves are guaranteed to be in their original and inalterable state
+ - since: %s (Sequence Number: %s)
+ - to: %s (Sequence Number: %s)'''
+ ) % (start_move_info[0], start_move_info[1], end_move_info[0], end_move_info[1]))
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.line"
+
+ @api.multi
+ def write(self, vals):
+ # restrict the operation in case we are trying to write a forbidden field
+ if set(vals).intersection(LINE_FIELDS):
+ if any(l.company_id.country_id.code == 'FR' and l.move_id.state == 'posted' for l in self):
+ raise UserError(ERR_MSG % (self._name, ', '.join(LINE_FIELDS)))
+ return super(AccountMoveLine, self).write(vals)
+
+
+class AccountJournal(models.Model):
+ _inherit = "account.journal"
+
+ @api.multi
+ def write(self, vals):
+ # restrict the operation in case we are trying to write a forbidden field
+ if self.company_id.country_id.code == 'FR' and vals.get('update_posted'):
+ raise UserError(ERR_MSG % (self._name, 'update_posted'))
+ return super(AccountJournal, self).write(vals)
+
+ @api.model
+ def create(self, vals):
+ # restrict the operation in case we are trying to set a forbidden field
+ if self.company_id.country_id.code == 'FR' and vals.get('update_posted'):
+ raise UserError(ERR_MSG % (self._name, 'update_posted'))
+ return super(AccountJournal, self).create(vals)
diff --git a/addons/l10n_fr_certification/models/res_company.py b/addons/l10n_fr_certification/models/res_company.py
new file mode 100644
index 00000000000..66a2a633164
--- /dev/null
+++ b/addons/l10n_fr_certification/models/res_company.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import fields, models, api
+
+class ResCompany(models.Model):
+ _inherit = 'res.company'
+
+ l10n_fr_secure_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to ensure the securisation of data', readonly=True)
+
+ @api.model
+ def create(self, vals):
+ company = super(ResCompany, self).create(vals)
+ #when creating a new french company, create the securisation sequence as well
+ if company.country_id == self.env.ref('base.fr'):
+ company._create_secure_sequence()
+ return company
+
+ @api.multi
+ def write(self, vals):
+ res = super(ResCompany, self).write(vals)
+ #if country changed to fr, create the securisation sequence
+ if vals.get('country_id') and vals.get('country_id') == self.env.ref('base.fr').id:
+ self.filtered(lambda c: not c.l10n_fr_secure_sequence_id)._create_secure_sequence()
+ return res
+
+ def _create_secure_sequence(self):
+ """This function creates a no_gap sequence on each companies in self that will ensure
+ a unique number is given to all posted account.move in such a way that we can always
+ find the previous move of a journal entry.
+ """
+ for company in self:
+ vals = {
+ 'name': 'French Securisation of account_move_line - ' + company.name,
+ 'code': 'FRSECUR',
+ 'implementation': 'no_gap',
+ 'prefix': '',
+ 'suffix': '',
+ 'padding': 0,
+ 'company_id': company.id}
+ seq = self.env['ir.sequence'].create(vals)
+ company.write({'l10n_fr_secure_sequence_id': seq.id})
diff --git a/addons/l10n_fr_certification/views/no_cancel.xml b/addons/l10n_fr_certification/views/no_cancel.xml
new file mode 100644
index 00000000000..43bc66d6ee3
--- /dev/null
+++ b/addons/l10n_fr_certification/views/no_cancel.xml
@@ -0,0 +1,36 @@
+<odoo>
+ <record id="bank_statement_draft_form_inherit" model="ir.ui.view">
+ <field name="name">bank.statement.draft.form.inherit</field>
+ <field name="model">account.bank.statement</field>
+ <field name="inherit_id" ref="account.view_bank_statement_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='button_cancel']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="invoice_form_no_cancel_inherit" model="ir.ui.view">
+ <field name="name">invoice.form.cancel.inherit</field>
+ <field name="model">account.invoice</field>
+ <field name="inherit_id" ref="account.invoice_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='action_invoice_cancel']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_move_form_no_cancel_inherit" model="ir.ui.view">
+ <field name="name">account.move.form.nocancel.inherit</field>
+ <field name="model">account.move</field>
+ <field name="inherit_id" ref="account.view_move_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='button_cancel']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+
+ </field>
+ </record>
+
+</odoo>
diff --git a/addons/l10n_in_schedule6/__openerp__.py b/addons/l10n_in_schedule6/__manifest__.py
index 20b803525d1..20b803525d1 100644
--- a/addons/l10n_in_schedule6/__openerp__.py
+++ b/addons/l10n_in_schedule6/__manifest__.py
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 1fc922a0d55..17539e1023b 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -58,6 +58,21 @@ def format_tz(env, dt, tz=False, format=False):
ftime = ts.strftime(format_time).decode('utf-8')
return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '')
+def format_amount(env, amount, currency):
+ fmt = "%.{0}f".format(currency.decimal_places)
+ lang = env['res.lang']._lang_get(env.context.get('lang') or 'en_US')
+
+ formatted_amount = lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
+ .replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011')
+
+ pre = post = u''
+ if currency.position == 'before':
+ pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=currency.symbol or '')
+ else:
+ post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=currency.symbol or '')
+
+ return u'{pre}{0}{post}'.format(formatted_amount, pre=pre, post=post)
+
try:
# We use a jinja2 sandboxed environment to render mako templates.
# Note that the rendering does not cover all the mako syntax, in particular
@@ -362,6 +377,7 @@ class MailTemplate(models.Model):
variables = {
'format_date': lambda date, format=False, context=self._context: format_date(self.env, date, format),
'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format),
+ 'format_amount': lambda amount, currency, context=self._context: format_amount(self.env, amount, currency),
'user': self.env.user,
'ctx': self._context, # context kw would clash with mako internals
}
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 1bec12c7eb1..48cf795bed1 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -5,6 +5,7 @@ from datetime import timedelta
from functools import partial
import psycopg2
+import pytz
from odoo import api, fields, models, tools, _
from odoo.tools import float_is_zero
@@ -699,7 +700,7 @@ class PosOrder(models.Model):
qty_done = pack_operation.product_qty
pack_operation.write({'pack_lot_ids': map(lambda x: (0, 0, x), pack_lots), 'qty_done': qty_done})
- def add_payment(self, data):
+ def _prepare_bank_statement_line_payment_values(self, data):
"""Create a new payment for the order"""
args = {
'amount': data['amount'],
@@ -744,8 +745,16 @@ class PosOrder(models.Model):
'journal_id': journal_id,
'ref': self.session_id.name,
})
+
+ return args
+
+ def add_payment(self, data):
+ """Create a new payment for the order"""
+ args = self._prepare_bank_statement_line_payment_values(data)
+ context = dict(self.env.context)
+ context.pop('pos_session_id', False)
self.env['account.bank.statement.line'].with_context(context).create(args)
- return statement_id
+ return args.get('statement_id', False)
@api.multi
def refund(self):
@@ -918,7 +927,9 @@ class ReportSaleDetails(models.AbstractModel):
if not configs:
configs = self.env['pos.config'].search([])
- today = fields.Datetime.from_string(fields.Date.context_today(self))
+ user_tz = pytz.timezone(self.env.context.get('tz') or self.env.user.tz or 'UTC')
+ today = user_tz.localize(fields.Datetime.from_string(fields.Date.context_today(self)))
+ today = today.astimezone(pytz.timezone('UTC'))
if date_start:
date_start = fields.Datetime.from_string(date_start)
else:
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 735d5e3e599..535db66e093 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -246,16 +246,22 @@ class PosSession(models.Model):
@api.multi
def action_pos_session_closing_control(self):
+ self._check_pos_session_balance()
for session in self:
- for statement in session.statement_ids:
- if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real):
- statement.write({'balance_end_real': statement.balance_end})
session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()})
if not session.config_id.cash_control:
session.action_pos_session_close()
@api.multi
+ def _check_pos_session_balance(self):
+ for session in self:
+ for statement in session.statement_ids:
+ if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real):
+ statement.write({'balance_end_real': statement.balance_end})
+
+ @api.multi
def action_pos_session_validate(self):
+ self._check_pos_session_balance()
self.action_pos_session_close()
@api.multi
diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart
index e2b61e2f591..15908b4c5aa 100755
--- a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart
+++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart
@@ -3,4 +3,11 @@ xset s off
xset -dpms
export HOME=/tmp
-/usr/bin/firefox http://localhost:8069/point_of_sale/display &
+
+DISPLAY_URL='http://localhost:8069/point_of_sale/display'
+
+# wait until odoo is ready, otherwise you will have to plug a
+# keyboard into the posbox just to refresh firefox
+until wget -q "${DISPLAY_URL}" -O /dev/null ; do sleep 1 ; done
+
+/usr/bin/firefox "${DISPLAY_URL}" &
diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml
index 2e6a6d0a5bd..4c7698b5cac 100644
--- a/addons/point_of_sale/views/pos_order_view.xml
+++ b/addons/point_of_sale/views/pos_order_view.xml
@@ -38,7 +38,7 @@
<field name="price_unit" widget="monetary"/>
<field name="discount" widget="monetary"/>
<field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
- <field name="tax_ids" invisible="1"/>
+ <field name="tax_ids" widget="many2many_tags" invisible="1"/>
<field name="price_subtotal" widget="monetary"/>
<field name="price_subtotal_incl" widget="monetary"/>
</tree>
@@ -51,7 +51,7 @@
<field name="price_subtotal" invisible="1" widget="monetary"/>
<field name="price_subtotal_incl" invisible="1" widget="monetary"/>
<field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
- <field name="tax_ids" invisible="1"/>
+ <field name="tax_ids" widget="many2many_tags" invisible="1"/>
<field name="notice"/>
</group>
</form>
diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js
index ec118ad4637..27f5c98f9ae 100644
--- a/addons/pos_restaurant/static/src/js/floors.js
+++ b/addons/pos_restaurant/static/src/js/floors.js
@@ -840,12 +840,13 @@ models.PosModel = models.PosModel.extend({
add_new_order: function() {
if (this.config.iface_floorplan) {
if (this.table) {
- _super_posmodel.add_new_order.call(this);
+ return _super_posmodel.add_new_order.call(this);
} else {
console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode");
+ return undefined;
}
} else {
- _super_posmodel.add_new_order.apply(this,arguments);
+ return _super_posmodel.add_new_order.apply(this,arguments);
}
},
diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py
index 5a76a14c64f..5415f85e5f4 100644
--- a/addons/resource/tests/test_resource.py
+++ b/addons/resource/tests/test_resource.py
@@ -279,6 +279,7 @@ class ResourceWorkingHours(TestResourceCommon):
res = self.calendar.plan_hours(-40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
self.assertEqual(res, Datetime.from_string('2013-01-29 09:00:00'))
+
def test_calendar_hours_scheduling_forward(self):
res = self.calendar._schedule_hours(40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
self.assertEqual(res[0][:2], (Datetime.from_string('2013-02-12 09:00:00'), Datetime.from_string('2013-02-12 16:00:00')))
@@ -289,6 +290,9 @@ class ResourceWorkingHours(TestResourceCommon):
self.assertEqual(res[5][:2], (Datetime.from_string('2013-02-22 16:00:00'), Datetime.from_string('2013-02-22 23:00:00')))
self.assertEqual(res[6][:2], (Datetime.from_string('2013-02-26 08:00:00'), Datetime.from_string('2013-02-26 09:00:00')))
+ res = self.calendar.schedule_hours_get_date(40, day_dt=self.date1.replace(minute=0, second=0))
+ self.assertEqual(res, Datetime.from_string('2013-02-26 09:00:00'))
+
td = timedelta()
for item in res:
td += item[1] - item[0]
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 3ca93dd3b90..f35bc650838 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -685,14 +685,21 @@ class SaleOrderLine(models.Model):
return {}
@api.model
- def create(self, values):
+ def _prepare_add_missing_fields(self, values):
+ """ Deduce missing required fields from the onchange """
+ res = {}
onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_id']
if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields):
line = self.new(values)
line.product_id_change()
for field in onchange_fields:
if field not in values:
- values[field] = line._fields[field].convert_to_write(line[field], line)
+ res[field] = line._fields[field].convert_to_write(line[field], line)
+ return res
+
+ @api.model
+ def create(self, values):
+ values.update(self._prepare_add_missing_fields(values))
line = super(SaleOrderLine, self).create(values)
if line.state == 'sale':
line._action_procurement_create()
diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py
index 63dc3f872f0..dfb24840f2d 100644
--- a/addons/sale_margin/models/sale_order.py
+++ b/addons/sale_margin/models/sale_order.py
@@ -42,6 +42,8 @@ class SaleOrderLine(models.Model):
@api.model
def create(self, vals):
+ vals.update(self._prepare_add_missing_fields(vals))
+
# Calculation of the margin for programmatic creation of a SO line. It is therefore not
# necessary to call product_id_change_margin manually
if 'purchase_price' not in vals:
diff --git a/addons/sale_margin/tests/test_sale_margin.py b/addons/sale_margin/tests/test_sale_margin.py
index 46cf3561aec..8252fd7f390 100644
--- a/addons/sale_margin/tests/test_sale_margin.py
+++ b/addons/sale_margin/tests/test_sale_margin.py
@@ -23,14 +23,22 @@ class TestSaleMargin(common.TransactionCase):
sale_order_so11 = self.SaleOrder.create({
'date_order': datetime.today(),
'name': 'Test_SO011',
- 'order_line': [(0, 0, {
- 'name': '[CARD] Graphics Card',
- 'purchase_price': 700.0,
- 'price_unit': 1000.0,
- 'product_uom': self.product_uom_id,
- 'product_uom_qty': 10.0,
- 'state': 'draft',
- 'product_id': self.product_id})],
+ 'order_line': [
+ (0, 0, {
+ 'name': '[CARD] Graphics Card',
+ 'purchase_price': 700.0,
+ 'price_unit': 1000.0,
+ 'product_uom': self.product_uom_id,
+ 'product_uom_qty': 10.0,
+ 'state': 'draft',
+ 'product_id': self.product_id}),
+ (0, 0, {
+ 'name': 'Line without product_uom',
+ 'price_unit': 1000.0,
+ 'purchase_price': 700.0,
+ 'product_uom_qty': 10.0,
+ 'state': 'draft',
+ 'product_id': self.product_id})],
'partner_id': self.partner_id,
'partner_invoice_id': self.partner_invoice_address_id,
'partner_shipping_id': self.partner_invoice_address_id,
@@ -38,4 +46,4 @@ class TestSaleMargin(common.TransactionCase):
# Confirm the sales order.
sale_order_so11.action_confirm()
# Verify that margin field gets bind with the value.
- self.assertEqual(sale_order_so11.margin, 3000.00, "Sales order margin should be 3000.00")
+ self.assertEqual(sale_order_so11.margin, 6000.00, "Sales order margin should be 6000.00")
diff --git a/addons/web_kanban/static/src/less/kanban_dashboard.less b/addons/web_kanban/static/src/less/kanban_dashboard.less
index eb036d0c0bd..9c7c9c76658 100644
--- a/addons/web_kanban/static/src/less/kanban_dashboard.less
+++ b/addons/web_kanban/static/src/less/kanban_dashboard.less
@@ -39,7 +39,7 @@
// If invisible, elem will no take more than its original height
a, span {
- .o-text-overflow(block);
+ .o-text-overflow(inline-block);
position: relative;
}
@@ -88,9 +88,10 @@
.o_kanban_manage_button_section {
.o-flex(1, 0, 0);
- padding: @odoo-horizontal-padding;
.o_kanban_manage_toggle_button {
+ display: block;
+ padding: @odoo-horizontal-padding;
text-align: right;
}
}
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 1e65fd256a8..c62486d34a0 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -179,8 +179,8 @@ class Http(models.AbstractModel):
nearest_lang = not func and cls.get_nearest_lang(path[1])
url_lang = nearest_lang and path[1]
preferred_lang = ((cook_lang if cook_lang in langs else False)
- or request.website.default_lang_code
- or (not is_a_bot and cls.get_nearest_lang(request.lang)))
+ or (not is_a_bot and cls.get_nearest_lang(request.lang))
+ or request.website.default_lang_code)
request.lang = context['lang'] = nearest_lang or preferred_lang
# if lang in url but not the displayed or default language --> change or remove
diff --git a/addons/website_sale_options/static/src/js/website_sale.js b/addons/website_sale_options/static/src/js/website_sale.js
index e206184b52e..7de8faff3ca 100644
--- a/addons/website_sale_options/static/src/js/website_sale.js
+++ b/addons/website_sale_options/static/src/js/website_sale.js
@@ -52,6 +52,11 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
$modal.modal('hide');
});
+ $modal.on('click', '.css_attribute_color input', function (event) {
+ $modal.find('.css_attribute_color').removeClass("active");
+ $modal.find('.css_attribute_color:has(input:checked)').addClass("active");
+ });
+
$modal.on("click", "a.js_add, a.js_remove", function (event) {
event.preventDefault();
var $parent = $(this).parents('.js_product:first');