diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 7cf32537e81..3c22140de54 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1329,6 +1329,7 @@ class AccountInvoice(models.Model): values['state'] = 'draft' values['number'] = False values['origin'] = invoice.number + values['payment_term_id'] = False values['refund_invoice_id'] = invoice.id if date: @@ -1714,6 +1715,7 @@ class AccountPaymentTerm(models.Model): def compute(self, value, date_ref=False): date_ref = date_ref or fields.Date.today() amount = value + sign = value < 0 and -1 or 1 result = [] if self.env.context.get('currency_id'): currency = self.env['res.currency'].browse(self.env.context['currency_id']) @@ -1721,7 +1723,7 @@ class AccountPaymentTerm(models.Model): currency = self.env.user.company_id.currency_id for line in self.line_ids: if line.value == 'fixed': - amt = currency.round(line.value_amount) + amt = sign * currency.round(line.value_amount) elif line.value == 'percent': amt = currency.round(value * (line.value_amount / 100.0)) elif line.value == 'balance': diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 5a80b5d98ff..3b00f72bb87 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -924,7 +924,7 @@ class AccountMoveLine(models.Model): elif self._context.get('skip_full_reconcile_check') == 'amount_currency_only': field = 'amount_residual_currency' #target the pair of move in self that are the oldest - sorted_moves = sorted(self, key=lambda a: a.date) + sorted_moves = sorted(self, key=lambda a: a.date_maturity or a.date) debit = credit = False for aml in sorted_moves: if credit and debit: diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index 974b4c9acf6..2bcfddd39d1 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -111,13 +111,15 @@ class Employee(models.Model): active = fields.Boolean('Active', related='resource_id.active', default=True, store=True) # private partner address_home_id = fields.Many2one( - 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.') + 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.', + groups="hr.group_hr_user") is_address_home_a_company = fields.Boolean( 'The employee adress has a company linked', compute='_compute_is_address_home_a_company', + groups="hr.group_hr_user", ) country_id = fields.Many2one( - 'res.country', 'Nationality (Country)') + 'res.country', 'Nationality (Country)', groups="hr.group_hr_user") gender = fields.Selection([ ('male', 'Male'), ('female', 'Female'), @@ -139,9 +141,9 @@ class Employee(models.Model): domain="[('partner_id', '=', address_home_id)]", groups="hr.group_hr_user", help='Employee bank salary account') - permit_no = fields.Char('Work Permit No') - visa_no = fields.Char('Visa No') - visa_expire = fields.Date('Visa Expire Date') + permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user") + visa_no = fields.Char('Visa No', groups="hr.group_hr_user") + visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user") # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( diff --git a/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py b/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py index a37770d2c1c..44367687c8f 100644 --- a/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py +++ b/addons/l10n_fr/migrations/2.0/post-migrate_tags_on_taxes.py @@ -1,7 +1,5 @@ -from openerp.modules.registry import RegistryManager - +# -*- coding: utf-8 -*- +from odoo.addons.account.models.chart_template import migrate_tags_on_taxes def migrate(cr, version): - registry = RegistryManager.get(cr.dbname) - from openerp.addons.account.models.chart_template import migrate_tags_on_taxes - migrate_tags_on_taxes(cr, registry) + migrate_tags_on_taxes(cr, None) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index e7ce561d56f..ba856c4cfab 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -364,15 +364,22 @@ class PosOrder(models.Model): for order in self: aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id) + + # Reconcile returns first + # to avoid mixing up the credit of a payment and the credit of a return + # in the receivable account + aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit)) try: - aml.reconcile() + aml_returns.reconcile() + (aml - aml_returns).reconcile() except Exception: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic # reconciliation is introduced for convenience, not for mandatory accounting # reasons. - _logger.error('Reconciliation did not work for order %s', order.name) - continue + # It may be interesting to have the Traceback logged anyway + # for debugging and support purposes + _logger.exception('Reconciliation did not work for order %s', order.name) def _default_session(self): return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index 2d3215c627e..cf767d1962f 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -28,7 +28,7 @@ access_product_pricelist_manager,product.pricelist manager,product.model_product access_product_pricelist_user,product.pricelist user,product.model_product_pricelist,group_pos_user,1,0,0,0 access_pos_session_user,pos.session user,model_pos_session,group_pos_user,1,1,1,0 access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,1,0,0 -access_pos_config_manager,pos.config user,model_pos_config,group_pos_manager,1,1,1,0 +access_pos_config_manager,pos.config manager,model_pos_config,group_pos_manager,1,1,1,1 access_product_category_pos_manager,pos.category manager,model_pos_category,group_pos_manager,1,1,1,1 access_product_category_pos_user,pos.category user,model_pos_category,group_pos_user,1,0,0,0 access_barcode_nomenclature_pos_user,barcode.nomenclature.pos.user,barcodes.model_barcode_nomenclature,group_pos_user,1,0,0,0 diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js index 2112468fa95..80bb28a4c01 100644 --- a/addons/web_editor/static/src/js/widgets/ace.js +++ b/addons/web_editor/static/src/js/widgets/ace.js @@ -192,7 +192,6 @@ var ViewEditor = Widget.extend({ */ start: function () { this.$viewEditor = this.$('#ace-view-editor'); - this.$editor = this.$('.ace_editor'); this.$typeSwitcherChoices = this.$('.o_ace_type_switcher_choice'); this.$typeSwitcherBtn = this.$('.o_ace_type_switcher > .dropdown-toggle'); @@ -210,6 +209,7 @@ var ViewEditor = Widget.extend({ this.aceEditor = window.ace.edit(this.$viewEditor[0]); this.aceEditor.setTheme('ace/theme/monokai'); + this.$editor = this.$('.ace_editor'); var refX = 0; var resizing = false; diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index e3aa0f60f90..64928682533 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -207,11 +207,12 @@ class BlogPost(models.Model): @api.multi def write(self, vals): - self.ensure_one() - if 'website_published' in vals and 'published_date' not in vals: - if (self.published_date or '') <= fields.Datetime.now(): - vals['published_date'] = vals['website_published'] and fields.Datetime.now() - result = super(BlogPost, self).write(vals) + result = True + for post in self: + copy_vals = vals + if 'website_published' in vals and 'published_date' not in vals and (post.published_date or '') <= fields.Datetime.now(): + copy_vals['published_date'] = vals['website_published'] and fields.Datetime.now() or False + result &= super(BlogPost, self).write(copy_vals) self._check_for_publication(vals) return result diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 819176096b2..eaf17861741 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -15,7 +15,7 @@ class WebsiteEventTrackController(http.Controller): @http.route(['''/event//track//agenda'''], type='http', auth="public", website=True, sitemap=False) def event_agenda(self, event, tag=None, **post): + event = event.with_context(tz=event.date_tz or 'UTC') local_tz = pytz.timezone(event.date_tz or 'UTC') days_tracks = collections.defaultdict(lambda: []) for track in event.track_ids.sorted(lambda track: (track.date or '', bool(track.location_id))): @@ -93,6 +94,7 @@ class WebsiteEventTrackController(http.Controller): '''/event//track/tag/''' ], type='http', auth="public", website=True, sitemap=False) def event_tracks(self, event, tag=None, **post): + event = event.with_context(tz=event.date_tz or 'UTC') searches = {} if tag: searches.update(tag=tag.id) diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml index 9d5bee401ba..70ed14e680a 100644 --- a/addons/website_event_track/views/event_track_templates.xml +++ b/addons/website_event_track/views/event_track_templates.xml @@ -255,7 +255,9 @@
Date
-
+ + () +
Duration

Location
diff --git a/doc/cla/individual/sergeiruzkiicode.md b/doc/cla/individual/sergeiruzkiicode.md new file mode 100644 index 00000000000..44af0359a97 --- /dev/null +++ b/doc/cla/individual/sergeiruzkiicode.md @@ -0,0 +1,11 @@ +Belarus, 2017-01-17 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Sergei Ruzki sergei.ruzki@icode.by https://github.com/sergeiruzkiicode diff --git a/odoo/addons/base/res/res_country_data.xml b/odoo/addons/base/res/res_country_data.xml index 819082fb27d..008f8b7e995 100644 --- a/odoo/addons/base/res/res_country_data.xml +++ b/odoo/addons/base/res/res_country_data.xml @@ -254,8 +254,8 @@ Belarus by - - + + Belize diff --git a/odoo/addons/base/res/res_currency_data.xml b/odoo/addons/base/res/res_currency_data.xml index f286190872d..2d32fedb9c5 100644 --- a/odoo/addons/base/res/res_currency_data.xml +++ b/odoo/addons/base/res/res_currency_data.xml @@ -516,10 +516,19 @@ BR 0.01 - Ruble + Ruble BYR Kapeyka + + BYN + Br + 0.01 + + Rubles + Kopeks + + BZD BZ$ diff --git a/odoo/addons/base/res/res_currency_demo.xml b/odoo/addons/base/res/res_currency_demo.xml index 8ca63297a3d..220e7849668 100644 --- a/odoo/addons/base/res/res_currency_demo.xml +++ b/odoo/addons/base/res/res_currency_demo.xml @@ -349,6 +349,12 @@ 10228.19 + + + 2016-07-01 + 2.23151 + + 2010-01-01 diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index 53a3438b21a..2915d9a0086 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -15,6 +15,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro from odoo.osv import expression from odoo.service.db import check_super from odoo.tools import partition, pycompat +from odoo.http import root _logger = logging.getLogger(__name__) @@ -372,6 +373,8 @@ class Users(models.Model): if any(key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): + # force deletion of all sessions for these users + root.session_store.delete_sessions_for_uids(self.ids) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) @@ -385,7 +388,10 @@ class Users(models.Model): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) - return super(Users, self).unlink() + res = super(Users, self).unlink() + # force deletion of all sessions for these users + root.session_store.delete_sessions_for_uids(self.ids) + return res @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): diff --git a/odoo/http.py b/odoo/http.py index 7524595406a..eae6b69a76a 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1030,7 +1030,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.db = db self.uid = uid self.login = login - self.password = password request.uid = uid request.disable_db = False @@ -1045,7 +1044,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): """ if not self.db or not self.uid: raise SessionExpiredException("Session expired") - security.check(self.db, self.uid, self.password) def logout(self, keep_db=False): for k in list(self): @@ -1058,7 +1056,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.setdefault("db", None) self.setdefault("uid", None) self.setdefault("login", None) - self.setdefault("password", None) self.setdefault("context", {}) def get_context(self): @@ -1271,6 +1268,17 @@ class DisableCacheMiddleware(object): start_response(status, new_headers) return self.app(environ, start_wrapped) +class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore): + def delete_sessions_for_uids(self, uids): + # pretty expensive on large session stores, especially non-local! + uids = set(uids) + _logger.info('Deleting all HTTP sessions for UIDs %s', uids) + for sid in self.list(): + s = self.get(sid) + if s.uid and s.uid in uids: + _logger.debug('Deleting session %s', sid) + self.delete(s) + class Root(object): """Root WSGI application for the OpenERP Web Client. """ @@ -1282,7 +1290,7 @@ class Root(object): # Setup http sessions path = odoo.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) - return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) + return OdooSessionStore(path, session_class=OpenERPSession) @lazy_property def nodb_routing_map(self):