diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 5bea59342b2..291be1aa22e 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -644,8 +644,8 @@ class AccountBankStatementLine(models.Model): # Create The payment payment = self.env['account.payment'] + partner_id = self.partner_id or (aml_dict.get('move_line') and aml_dict['move_line'].partner_id) or self.env['res.partner'] if abs(total)>0.00001: - partner_id = self.partner_id and self.partner_id.id or False partner_type = False if partner_id and len(account_types) == 1: partner_type = 'customer' if account_types == receivable_account_type else 'supplier' @@ -660,7 +660,7 @@ class AccountBankStatementLine(models.Model): payment = self.env['account.payment'].create({ 'payment_method_id': payment_methods and payment_methods[0].id or False, 'payment_type': total >0 and 'inbound' or 'outbound', - 'partner_id': self.partner_id and self.partner_id.id or False, + 'partner_id': partner_id.id, 'partner_type': partner_type, 'journal_id': self.statement_id.journal_id.id, 'payment_date': self.date, diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index e534ab89eb5..4da13596b88 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -665,12 +665,12 @@ class AccountInvoice(models.Model): msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.') raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) - if type in ('out_invoice', 'out_refund'): - account_id = rec_account.id - payment_term_id = p.property_payment_term_id.id - else: + if type in ('in_invoice', 'in_refund'): account_id = pay_account.id payment_term_id = p.property_supplier_payment_term_id.id + else: + account_id = rec_account.id + payment_term_id = p.property_payment_term_id.id delivery_partner_id = self.get_delivery_partner_id() fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=delivery_partner_id) diff --git a/addons/account/tests/test_account_invoice_rounding.py b/addons/account/tests/test_account_invoice_rounding.py index f0a7774d61d..38c7193a2f1 100644 --- a/addons/account/tests/test_account_invoice_rounding.py +++ b/addons/account/tests/test_account_invoice_rounding.py @@ -3,6 +3,7 @@ from odoo.addons.account.tests.account_test_classes import AccountingTestCase from odoo.exceptions import ValidationError from odoo.tests import tagged +from odoo.tools import mute_logger import time @@ -60,7 +61,8 @@ class TestAccountInvoiceRounding(AccountingTestCase): # to avoid a ValidationError from _check_cash_rounding because the onchange # are not well triggered in the tests. try: - invoice_id.cash_rounding_id = cash_rounding_id + with mute_logger('odoo.models'): + invoice_id.cash_rounding_id = cash_rounding_id except ValidationError: pass diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 03df5bd89ce..9f1190b934c 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -8,6 +8,7 @@ from collections import defaultdict from datetime import datetime, timedelta from odoo import api, exceptions, fields, models, _ +from odoo.tools import pycompat class SignupError(Exception): pass @@ -28,16 +29,16 @@ class ResPartner(models.Model): signup_token = fields.Char(copy=False, groups="base.group_erp_manager") signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager") signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager") - signup_valid = fields.Boolean(compute='_compute_signup_valid', compute_sudo=True, string='Signup Token is Valid') + signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid') signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL') @api.multi @api.depends('signup_token', 'signup_expiration') def _compute_signup_valid(self): dt = now() - for partner in self: - partner.signup_valid = bool(partner.signup_token) and \ - (not partner.signup_expiration or dt <= partner.signup_expiration) + for partner, partner_sudo in pycompat.izip(self, self.sudo()): + partner.signup_valid = bool(partner_sudo.signup_token) and \ + (not partner_sudo.signup_expiration or dt <= partner_sudo.signup_expiration) @api.multi def _compute_signup_url(self): diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 2ac4deaed5f..2834dfb11c9 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -64,3 +64,17 @@ class SaleOrderLine(models.Model): @api.onchange('event_ticket_id') def _onchange_event_ticket_id(self): self.price_unit = (self.event_id.company_id or self.env.user.company_id).currency_id.compute(self.event_ticket_id.price, self.order_id.currency_id) + + @api.onchange('product_uom', 'product_uom_qty') + def product_uom_change(self): + if not self.event_ticket_id: + super(SaleOrderLine, self).product_uom_change() + + @api.multi + @api.onchange('product_id') + def product_id_change(self): + super(SaleOrderLine, self).product_id_change() + self.event_id = None + self.event_ticket_id = None + if self.product_id.event_ok: + self.price_unit = 0.0 diff --git a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py index 5bbb6848bd3..4c66db60db9 100644 --- a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py +++ b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py @@ -10,7 +10,7 @@ class GamificationBadgeUserWizard(models.TransientModel): employee_id = fields.Many2one('hr.employee', string='Employee', required=True) user_id = fields.Many2one('res.users', string='User', - related='employee_id.user_id', store=True, readonly=True) + related='employee_id.user_id', store=True, readonly=True, compute_sudo=True) # TODO 12.0/master remove this hack by changing the model @api.model diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 35286c1fc9d..2a9fb451f33 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -92,7 +92,7 @@ class HolidaysRequest(models.Model): payslip_status = fields.Boolean('Reported in last payslips', help='Green this button when the leave has been taken into account in the payslip.') report_note = fields.Text('HR Comments') - user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) + user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, required=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') date_to = fields.Datetime('End Date', readonly=True, copy=False, required=True, diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 032baf2d971..88284475764 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -77,6 +77,27 @@ class AccountFrFec(models.TransientModel): listrow = list(row) return listrow + def _get_company_legal_data(self, company): + """ + Dom-Tom are excluded from the EU's fiscal territory + Those regions do not have SIREN + sources: + https://www.service-public.fr/professionnels-entreprises/vosdroits/F23570 + http://www.douane.gouv.fr/articles/a11024-tva-dans-les-dom + """ + dom_tom_group = self.env.ref('l10n_fr.dom-tom') + is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped('code') + if not is_dom_tom and not company.vat: + raise Warning( + _("Missing VAT number for company %s") % company.name) + if not is_dom_tom and company.vat[0:2] != 'FR': + raise Warning( + _("FEC is for French companies only !")) + + return { + 'siren': company.vat[4:13] if not is_dom_tom else '', + } + @api.multi def generate_fec(self): self.ensure_one() @@ -88,6 +109,9 @@ class AccountFrFec(models.TransientModel): # 2) CSV files are easier to read/use for a regular accountant. # So it will be easier for the accountant to check the file before # sending it to the fiscal administration + company = self.env.user.company_id + company_legal_data = self._get_company_legal_data(company) + header = [ u'JournalCode', # 0 u'JournalLib', # 1 @@ -109,18 +133,7 @@ class AccountFrFec(models.TransientModel): u'Idevise', # 17 ] - company = self.env.user.company_id - if not company.vat: - raise Warning( - _("Missing VAT number for company %s") % company.name) - if company.vat[0:2] != 'FR': - raise Warning( - _("FEC is for French companies only !")) - - fecfile = io.BytesIO() - w = pycompat.csv_writer(fecfile, delimiter='|') - w.writerow(header) - + rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_xml_ref = self.env.ref('account.data_unaffected_earnings') unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once @@ -196,7 +209,8 @@ class AccountFrFec(models.TransientModel): else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') - w.writerow(listrow) + rows_to_write.append(listrow) + #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results @@ -207,7 +221,7 @@ class AccountFrFec(models.TransientModel): if unaffected_earnings_account: unaffected_earnings_results[4] = unaffected_earnings_account.code unaffected_earnings_results[5] = unaffected_earnings_account.name - w.writerow(unaffected_earnings_results) + rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable sql_query = ''' @@ -265,7 +279,7 @@ class AccountFrFec(models.TransientModel): for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() - w.writerow(listrow) + rows_to_write.append(listrow) # LINES sql_query = ''' @@ -330,20 +344,19 @@ class AccountFrFec(models.TransientModel): sql_query, (self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): - w.writerow(list(row)) + rows_to_write.append(list(row)) - siren = company.vat[4:13] + fecvalue = self._csv_write_rows(rows_to_write) end_date = self.date_to.replace('-', '') suffix = '' if self.export_type == "nonofficial": suffix = '-NONOFFICIAL' - fecvalue = fecfile.getvalue() + self.write({ 'fec_data': base64.encodestring(fecvalue), # Filename = FECYYYYMMDD where YYYMMDD is the closing date - 'filename': '%sFEC%s%s.csv' % (siren, end_date, suffix), + 'filename': '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix), }) - fecfile.close() action = { 'name': 'FEC', @@ -352,3 +365,29 @@ class AccountFrFec(models.TransientModel): 'target': 'self', } return action + + def _csv_write_rows(self, rows, lineterminator=u'\r\n'): + """ + Write FEC rows into a file + It seems that Bercy's bureaucracy is not too happy about the + empty new line at the End Of File. + + @param {list(list)} rows: the list of rows. Each row is a list of strings + @param {unicode string} [optional] lineterminator: effective line terminator + Has nothing to do with the csv writer parameter + The last line written won't be terminated with it + + @return the value of the file + """ + fecfile = io.BytesIO() + writer = pycompat.csv_writer(fecfile, delimiter='|', lineterminator='') + + rows_length = len(rows) + for i, row in enumerate(rows): + if not i == rows_length - 1: + row.append(lineterminator) + writer.writerow(row) + + fecvalue = fecfile.getvalue() + fecfile.close() + return fecvalue diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 33c4b76d79d..36ced25ed75 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -84,8 +84,8 @@ class MailActivity(models.Model): activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity', domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict') - activity_category = fields.Selection(related='activity_type_id.category') - icon = fields.Char('Icon', related='activity_type_id.icon') + activity_category = fields.Selection(related='activity_type_id.category', readonly=True) + icon = fields.Char('Icon', related='activity_type_id.icon', readonly=True) summary = fields.Char('Summary') note = fields.Html('Note') feedback = fields.Html('Feedback') @@ -103,8 +103,8 @@ class MailActivity(models.Model): ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') - recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type") - previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type') + recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True) + previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True) has_recommended_activities = fields.Boolean( 'Next activities available', compute='_compute_has_recommended_activities', diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index ec28fb6fa15..ecbfedee859 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1477,6 +1477,8 @@ td { body { margin: 0; color: white !important; + /* avoid black background if backgrounds are printed */ + background: initial; } .oe_leftbar, .oe_loading, diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index 44eb1637619..9fa0e12561e 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -35,7 +35,7 @@ class PortalWizard(models.TransientModel): contact_ids = set() user_changes = [] for partner in self.env['res.partner'].sudo().browse(partner_ids): - contact_partners = partner.child_ids or [partner] + contact_partners = partner.child_ids | partner for contact in contact_partners: # make sure that each contact appears at most once in the list if contact.id not in contact_ids: diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index c0d1b404707..4f625948dfc 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -241,7 +241,11 @@ class Pricelist(models.Model): break # Final price conversion into pricelist currency if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist': - price = product.currency_id.compute(price, self.currency_id, round=False) + if suitable_rule.base == 'standard_price': + # The cost of the product is always in the company currency + price = product.cost_currency_id.compute(price, self.currency_id, round=False) + else: + price = product.currency_id.compute(price, self.currency_id, round=False) results[product.id] = (price, suitable_rule and suitable_rule.id or False) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 766aadefb37..8a31a68fc8b 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -62,6 +62,8 @@ class ProductTemplate(models.Model): currency_id = fields.Many2one( 'res.currency', 'Currency', compute='_compute_currency_id') + cost_currency_id = fields.Many2one( + 'res.currency', 'Cost Currency', compute='_compute_cost_currency_id') # price fields price = fields.Float( @@ -165,6 +167,10 @@ class ProductTemplate(models.Model): for template in self: template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id + def _compute_cost_currency_id(self): + for template in self: + template.cost_currency_id = self.env.user.company_id.currency_id.id + @api.multi def _compute_template_price(self): prices = {} diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index a47a49a87b4..1da4cdcc99e 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -51,13 +51,14 @@