diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-06-15 18:44:16 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-06-15 18:44:16 +0200 |
| commit | 1b50c829ef383705647d0c2df7c89e9051ec40fb (patch) | |
| tree | 036df47c7cf94424f04d7ff9dac371fb79e2b1e4 /addons | |
| parent | 0c2e018d4c416d0f47a3b64f1c28ce067dbab291 (diff) | |
| parent | 9713dc2e1f515b1260eaf26ec6bbfd5443f37d65 (diff) | |
[MERGE] forward port branch saas-15 up to 9713dc2e1f5
Diffstat (limited to 'addons')
28 files changed, 432 insertions, 38 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index e1dcd10bdec..f45f7ec6c70 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1322,9 +1322,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 6a49e33f547..313f0fa0f75 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -482,7 +482,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 fc7aaca4f56..b621247313c 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 2c989bb7e2e..2a7d191cbfc 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -61,6 +61,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 @@ -364,6 +379,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 1eefb33f88f..fff5cc24d84 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 @@ -701,7 +702,7 @@ class PosOrder(models.Model): pack_operation.write({'pack_lot_ids': map(lambda x: (0, 0, x), pack_lots), 'qty_done': qty_done}) return has_wrong_lots - 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'], @@ -746,8 +747,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): @@ -920,7 +929,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 9be61b0d2ed..f16551c2a13 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -250,16 +250,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 67307794fbc..19403e033d0 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -852,12 +852,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/models/resource.py b/addons/resource/models/resource.py index 32da153366d..c464fada7a1 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -527,7 +527,9 @@ class ResourceCalendar(models.Model): backwards = (hours < 0) intervals = [] remaining_hours, iterations = abs(hours * 1.0), 0 - current_datetime = day_dt + + day_dt_tz = to_naive_user_tz(day_dt, self.env.user) + current_datetime = day_dt_tz call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id) @@ -562,7 +564,11 @@ class ResourceCalendar(models.Model): def plan_hours(self, hours, day_dt, compute_leaves=False, resource_id=None): """ Return datetime after having planned hours """ res = self._schedule_hours(hours, day_dt, compute_leaves, resource_id) - return res and res[0][0] or False + if res and hours < 0.0: + return res[0][0] + elif res: + return res[-1][1] + return False @api.multi def _schedule_days(self, days, day_dt, compute_leaves=False, resource_id=None): @@ -582,7 +588,9 @@ class ResourceCalendar(models.Model): backwards = (days < 0) intervals = [] planned_days, iterations = 0, 0 - current_datetime = day_dt.replace(hour=0, minute=0, second=0, microsecond=0) + + day_dt_tz = to_naive_user_tz(day_dt, self.env.user) + current_datetime = day_dt_tz.replace(hour=0, minute=0, second=0, microsecond=0) while planned_days < abs(days) and iterations < 100: working_intervals = self._get_day_work_intervals( diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index 37e1d46fee7..b1a8ead484b 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -278,6 +278,10 @@ class ResourceWorkingHours(TestResourceCommon): td += item[1] - item[0] self.assertEqual(td.total_seconds() / 3600.0, 40.0) + 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'))) @@ -288,11 +292,25 @@ 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] self.assertEqual(td.total_seconds() / 3600.0, 40.0) + res = self.calendar.plan_hours(40, day_dt=Datetime.from_string('2013-02-12 09:00:00')) + self.assertEqual(res, Datetime.from_string('2013-02-26 09:00:00')) + + def test_calendar_hours_scheduling_timezone(self): + # user in timezone UTC-9 asks for work hours + self.env.user.tz = 'US/Alaska' + res = self.calendar.plan_hours( + 42, + to_naive_utc(Datetime.from_string('2013-02-12 09:25:00'), self.env.user)) + self.assertEqual(res, to_naive_utc(Datetime.from_string('2013-02-26 11:25:00'), self.env.user)) + def test_calendar_hours_scheduling_forward_leaves_resource(self): res = self.calendar._schedule_hours( 40, day_dt=Datetime.from_string('2013-02-12 09:00:00'), @@ -324,6 +342,11 @@ class ResourceWorkingHours(TestResourceCommon): compute_leaves=True, resource_id=self.resource1_id) self.assertEqual(res.date(), Datetime.from_string('2013-03-01 00:00:00').date(), 'resource_calendar: wrong days scheduling') + def test_calendar_days_scheduling_timezone(self): + self.env.user.tz = 'US/Alaska' + res = self.calendar.plan_days(5, to_naive_utc(Datetime.from_string('2013-02-12 09:08:07'), self.env.user)) + self.assertEqual(to_naive_user_tz(res, self.env.user).date(), Datetime.from_string('2013-02-26 00:00:00').date()) + WAR_START = date(1932, 11, 2) WAR_END = date(1932, 12, 10) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index a346fa1a318..361c7d279ff 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/static/src/less/kanban_dashboard.less b/addons/web/static/src/less/kanban_dashboard.less index fd544445f6d..e86350f6b49 100644 --- a/addons/web/static/src/less/kanban_dashboard.less +++ b/addons/web/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; } diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 014b10143f4..b6902b17c41 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/models/sale_order.py b/addons/website_sale/models/sale_order.py index c8a39dba78a..4c9ddca93b5 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -316,7 +316,7 @@ class Website(models.Model): @api.model def sale_get_payment_term(self, partner): DEFAULT_PAYMENT_TERM = 'account.account_payment_term_immediate' - return self.env.ref(DEFAULT_PAYMENT_TERM, False).id or partner.property_payment_term_id.id + return partner.property_payment_term_id.id or self.env.ref(DEFAULT_PAYMENT_TERM, False).id @api.multi def _prepare_sale_order_values(self, partner, pricelist): @@ -324,6 +324,7 @@ class Website(models.Model): affiliate_id = request.session.get('affiliate_id') salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id addr = partner.address_get(['delivery', 'invoice']) + default_user_id = partner.parent_id.user_id.id or partner.user_id.id values = { 'partner_id': partner.id, 'pricelist_id': pricelist.id, @@ -331,7 +332,7 @@ class Website(models.Model): 'team_id': self.salesteam_id.id, 'partner_invoice_id': addr['invoice'], 'partner_shipping_id': addr['delivery'], - 'user_id': salesperson_id or self.salesperson_id.id, + 'user_id': salesperson_id or self.salesperson_id.id or default_user_id, } company = self.company_id or pricelist.company_id if company: 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'); |
