diff --git a/addons/account/controllers/mail.py b/addons/account/controllers/mail.py index 499a2cdc..fe021f16 100644 --- a/addons/account/controllers/mail.py +++ b/addons/account/controllers/mail.py @@ -11,6 +11,7 @@ from flectra.tools.misc import consteq class MailController(MailController): + @classmethod def _redirect_to_record(cls, model, res_id, access_token=None): # If the current user doesn't have access to the invoice, but provided # a valid access token, redirect him to the front-end view. diff --git a/addons/account/data/mail_template_data.xml b/addons/account/data/mail_template_data.xml index d2751b72..25fee8a2 100644 --- a/addons/account/data/mail_template_data.xml +++ b/addons/account/data/mail_template_data.xml @@ -7,7 +7,7 @@ Invoicing: Invoice email - ${(object.user_id.email and '%s <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} + ${(object.user_id.email and '"%s" <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} ${object.company_id.name} Invoice (Ref ${object.number or 'n/a'}) ${object.partner_id.id} @@ -35,7 +35,7 @@ invoice % if object.origin: (with reference: ${object.origin}) % endif -amounting in ${object.amount_total} ${object.currency_id.name} +amounting in ${format_amount(object.amount_total, object.currency_id)} from ${object.company_id.name}.

@@ -71,7 +71,7 @@ from ${object.company_id.name}.
% set record = ctx.get('record') -% set company = record and record.company_id or user.company_id +% set company = record and record.company_id or ctx.get('company') diff --git a/addons/account/data/payment_receipt_data.xml b/addons/account/data/payment_receipt_data.xml index 82691942..66869e7d 100644 --- a/addons/account/data/payment_receipt_data.xml +++ b/addons/account/data/payment_receipt_data.xml @@ -14,7 +14,7 @@ ${object.partner_id.lang}

Dear ${object.partner_id.name},

-

Thank you for your payment.
Here is your payment receipt ${(object.name or '').replace('/','-')} amounting to ${object.amount} ${object.currency_id.name} from ${object.company_id.name}.

+

Thank you for your payment.
Here is your payment receipt ${(object.name or '').replace('/','-')} amounting to ${format_amount(object.amount, object.currency_id)} from ${object.company_id.name}.

If you have any questions, please do not hesitate to contact us.

Best regards, % if user and user.signature: diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 5ec4c3de..96ad474f 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -792,7 +792,7 @@ class AccountTax(models.Model): tag_ids = fields.Many2many('account.account.tag', 'account_tax_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting") tax_group_id = fields.Many2one('account.tax.group', string="Tax Group", default=_default_tax_group, required=True) # Technical field to make the 'tax_exigibility' field invisible if the same named field is set to false in 'res.company' model - hide_tax_exigibility = fields.Boolean(string='Hide Use Cash Basis Option', related='company_id.tax_exigibility') + hide_tax_exigibility = fields.Boolean(string='Hide Use Cash Basis Option', related='company_id.tax_exigibility', readonly=True) tax_exigibility = fields.Selection( [('on_invoice', 'Based on Invoice'), ('on_payment', 'Based on Payment'), diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 0d146d5e..01112b50 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -71,7 +71,7 @@ class AccountInvoice(models.Model): @api.onchange('amount_total') def _onchange_amount_total(self): for inv in self: - if inv.amount_total < 0: + if float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1: raise Warning(_('You cannot validate an invoice with a negative total amount. You should create a credit note instead.')) @api.model @@ -406,7 +406,7 @@ class AccountInvoice(models.Model): """ computes the prefix of the number that will be assigned to the first invoice/bill/refund of a journal, in order to let the user manually change it. """ - if not self.env.user._is_admin(): + if not self.env.user._is_system(): for invoice in self: invoice.sequence_number_next_prefix = False invoice.sequence_number_next = '' @@ -562,7 +562,10 @@ class AccountInvoice(models.Model): """ self.ensure_one() self.sent = True - return self.env.ref('account.account_invoices').report_action(self) + if self.user_has_groups('account.group_account_invoice'): + return self.env.ref('account.account_invoices').report_action(self) + else: + return self.env.ref('account.account_invoices_without_payment').report_action(self) @api.multi def action_invoice_sent(self): @@ -602,7 +605,8 @@ class AccountInvoice(models.Model): for invoice in self: # Delete non-manual tax lines self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,)) - self.invalidate_cache() + if self._cr.rowcount: + self.invalidate_cache() # Generate one tax line per tax, however many invoice lines it's applied to tax_grouped = invoice.get_taxes_values() @@ -783,7 +787,7 @@ class AccountInvoice(models.Model): to_open_invoices = self.filtered(lambda inv: inv.state != 'open') if to_open_invoices.filtered(lambda inv: inv.state != 'draft'): raise UserError(_("Invoice must be in draft state in order to validate it.")) - if to_open_invoices.filtered(lambda inv: inv.amount_total < 0): + if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1): raise UserError(_("You cannot validate an invoice with a negative total amount. You should create a credit note instead.")) to_open_invoices.action_date_assign() to_open_invoices.action_move_create() @@ -1523,6 +1527,13 @@ class AccountInvoiceLine(models.Model): return accounts['income'] return accounts['expense'] + def _set_currency(self): + company = self.invoice_id.company_id + currency = self.invoice_id.currency_id + if company and currency: + if company.currency_id != currency: + self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate + def _set_taxes(self): """ Used in on_change to set taxes and price.""" if self.invoice_id.type in ('out_invoice', 'out_refund'): @@ -1541,8 +1552,10 @@ class AccountInvoiceLine(models.Model): prec = self.env['decimal.precision'].precision_get('Product Price') if not self.price_unit or float_compare(self.price_unit, self.product_id.standard_price, precision_digits=prec) == 0: self.price_unit = fix_price(self.product_id.standard_price, taxes, fp_taxes) + self._set_currency() else: self.price_unit = fix_price(self.product_id.lst_price, taxes, fp_taxes) + self._set_currency() @api.onchange('product_id') def _onchange_product_id(self): @@ -1591,8 +1604,6 @@ class AccountInvoiceLine(models.Model): domain['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)] if company and currency: - if company.currency_id != currency: - self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate if self.uom_id and self.uom_id.id != product.uom_id.id: self.price_unit = product.uom_id._compute_price(self.price_unit, self.uom_id) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 48d31bd1..ba9c387e 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -56,7 +56,8 @@ class AccountMove(models.Model): total_amount += amount for partial_line in (line.matched_debit_ids + line.matched_credit_ids): total_reconciled += partial_line.amount - if float_is_zero(total_amount, precision_rounding=move.currency_id.rounding): + precision_currency = move.currency_id or move.company_id.currency_id + if float_is_zero(total_amount, precision_rounding=precision_currency.rounding): move.matched_percentage = 1.0 else: move.matched_percentage = total_reconciled / total_amount @@ -103,8 +104,7 @@ class AccountMove(models.Model): partner_id = fields.Many2one('res.partner', compute='_compute_partner_id', string="Partner", store=True, readonly=True) amount = fields.Monetary(compute='_amount_compute', store=True) narration = fields.Text(string='Internal Note') - company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, - default=lambda self: self.env.user.company_id) + company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True) matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") # Dummy Account field to search on account.move by account_id dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True) @@ -1044,8 +1044,6 @@ class AccountMoveLine(models.Model): raise UserError(_('Entries are not of the same account!')) if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'): raise UserError(_('The account %s (%s) is not marked as reconciliable !') % (all_accounts[0].name, all_accounts[0].code)) - if len(partners) > 1: - raise UserError(_('The partner has to be the same on all lines for receivable and payable accounts!')) #reconcile everything that can be remaining_moves = self.auto_reconcile_lines() @@ -1163,19 +1161,22 @@ class AccountMoveLine(models.Model): #create an empty move that will hold all the exchange rate adjustments exchange_move = False - if aml_to_balance_currency: + if aml_to_balance_currency and any([residual for dummy, residual in aml_to_balance_currency.values()]): exchange_move = self.env['account.move'].create( self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=self[0].company_id)) for currency, values in aml_to_balance_currency.items(): aml_to_balance = values[0] total_amount_currency = values[1] - #eventually create journal entries to book the difference due to foreign currency's exchange rate that fluctuates - aml_recs, partial_recs = self.env['account.partial.reconcile'].create_exchange_rate_entry(aml_to_balance, 0.0, total_amount_currency, currency, exchange_move) + if total_amount_currency: + #eventually create journal entries to book the difference due to foreign currency's exchange rate that fluctuates + aml_recs, partial_recs = self.env['account.partial.reconcile'].create_exchange_rate_entry(aml_to_balance, 0.0, total_amount_currency, currency, exchange_move) - #add the ecxhange rate line and the exchange rate partial reconciliation in the et of the full reconcile - self |= aml_recs - partial_rec_set |= partial_recs + #add the ecxhange rate line and the exchange rate partial reconciliation in the et of the full reconcile + self |= aml_recs + partial_rec_set |= partial_recs + else: + aml_to_balance.reconcile() if exchange_move: exchange_move.post() @@ -1203,6 +1204,7 @@ class AccountMoveLine(models.Model): rec_move_ids += account_move_line.matched_credit_ids if self.env.context.get('invoice_id'): current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id']) + aml_to_keep = current_invoice.move_id.line_ids | current_invoice.move_id.line_ids.mapped('full_reconcile_id.exchange_move_id.line_ids') rec_move_ids = rec_move_ids.filtered( lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids ) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index ae879cce..b2cd72bd 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -374,6 +374,12 @@ class account_payment(models.Model): self.destination_account_id = self.partner_id.property_account_receivable_id.id else: self.destination_account_id = self.partner_id.property_account_payable_id.id + elif self.partner_type == 'customer': + default_account = self.env['ir.property'].get('property_account_receivable_id', 'res.partner') + self.destination_account_id = default_account.id + elif self.partner_type == 'supplier': + default_account = self.env['ir.property'].get('property_account_payable_id', 'res.partner') + self.destination_account_id = default_account.id @api.onchange('partner_type') def _onchange_partner_type(self): @@ -429,12 +435,17 @@ class account_payment(models.Model): @api.multi def button_invoices(self): + if self.partner_type == 'supplier': + views = [(self.env.ref('account.invoice_supplier_tree').id, 'tree'), (self.env.ref('account.invoice_supplier_form').id, 'form')] + else: + views = [(self.env.ref('account.invoice_tree').id, 'tree'), (self.env.ref('account.invoice_form').id, 'form')] return { 'name': _('Paid Invoices'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.invoice', 'view_id': False, + 'views': views, 'type': 'ir.actions.act_window', 'domain': [('id', 'in', [x.id for x in self.invoice_ids])], } @@ -517,6 +528,7 @@ class account_payment(models.Model): (transfer_credit_aml + transfer_debit_aml).reconcile() rec.write({'state': 'posted', 'move_name': move.name}) + return True @api.multi def action_draft(self): diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index fa3e99d8..0ab56904 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -859,7 +859,7 @@ class WizardMultiChartsAccounts(models.TransientModel): @api.multi def existing_accounting(self, company_id): - model_to_check = ['account.move.line', 'account.invoice', 'account.move', 'account.payment', 'account.bank.statement'] + model_to_check = ['account.move.line', 'account.invoice', 'account.payment', 'account.bank.statement'] for model in model_to_check: if len(self.env[model].search([('company_id', '=', company_id.id)])) > 0: return True @@ -894,7 +894,7 @@ class WizardMultiChartsAccounts(models.TransientModel): accounting_props.unlink() # delete account, journal, tax, fiscal position and reconciliation model - models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.journal'] + models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.move', 'account.journal'] for model in models_to_delete: res = self.env[model].search([('company_id', '=', self.company_id.id)]) if len(res): diff --git a/addons/account/models/company.py b/addons/account/models/company.py index d9b9fe62..99099d11 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -2,10 +2,12 @@ from datetime import timedelta, datetime import calendar +import time +from dateutil.relativedelta import relativedelta from flectra import fields, models, api, _ -from flectra.exceptions import ValidationError from flectra.exceptions import UserError +from flectra.tools.misc import DEFAULT_SERVER_DATE_FORMAT from flectra.tools.float_utils import float_round, float_is_zero @@ -62,6 +64,61 @@ Best Regards,''')) account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.") account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.") + @api.multi + def _check_lock_dates(self, vals): + '''Check the lock dates for the current companies. This can't be done in a api.constrains because we need + to perform some comparison between new/old values. This method forces the lock dates to be irreversible. + + * You cannot define stricter conditions on advisors than on users. Then, the lock date on advisor must be set + after the lock date for users. + * You cannot lock a period that is not finished yet. Then, the lock date for advisors must be set after the + last day of the previous month. + * The new lock date for advisors must be set after the previous lock date. + + :param vals: The values passed to the write method. + ''' + period_lock_date = vals.get('period_lock_date') and\ + time.strptime(vals['period_lock_date'], DEFAULT_SERVER_DATE_FORMAT) + fiscalyear_lock_date = vals.get('fiscalyear_lock_date') and\ + time.strptime(vals['fiscalyear_lock_date'], DEFAULT_SERVER_DATE_FORMAT) + + previous_month = datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT) + relativedelta(months=-1) + days_previous_month = calendar.monthrange(previous_month.year, previous_month.month) + previous_month = previous_month.replace(day=days_previous_month[1]).timetuple() + for company in self: + old_fiscalyear_lock_date = company.fiscalyear_lock_date and\ + time.strptime(company.fiscalyear_lock_date, DEFAULT_SERVER_DATE_FORMAT) + + # The user attempts to remove the lock date for advisors + if old_fiscalyear_lock_date and not fiscalyear_lock_date and 'fiscalyear_lock_date' in vals: + raise ValidationError(_('The lock date for advisors is irreversible and can\'t be removed.')) + + # The user attempts to set a lock date for advisors prior to the previous one + if old_fiscalyear_lock_date and fiscalyear_lock_date and fiscalyear_lock_date < old_fiscalyear_lock_date: + raise ValidationError(_('The new lock date for advisors must be set after the previous lock date.')) + + # In case of no new fiscal year in vals, fallback to the oldest + if not fiscalyear_lock_date: + if old_fiscalyear_lock_date: + fiscalyear_lock_date = old_fiscalyear_lock_date + else: + continue + + # The user attempts to set a lock date for advisors prior to the last day of previous month + if fiscalyear_lock_date > previous_month: + raise ValidationError(_('You cannot lock a period that is not finished yet. Please make sure that the lock date for advisors is not set after the last day of the previous month.')) + + # In case of no new period lock date in vals, fallback to the one defined in the company + if not period_lock_date: + if company.period_lock_date: + period_lock_date = time.strptime(company.period_lock_date, DEFAULT_SERVER_DATE_FORMAT) + else: + continue + + # The user attempts to set a lock date for advisors prior to the lock date for users + if period_lock_date < fiscalyear_lock_date: + raise ValidationError(_('You cannot define stricter conditions on advisors than on users. Please make sure that the lock date on advisor is set before the lock date for users.')) + @api.model def _verify_fiscalyear_last_day(self, company_id, last_day, last_month): company = self.browse(company_id) diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index e234bf7e..c17963ee 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -444,3 +444,12 @@ class ResPartner(models.Model): action['domain'] = literal_eval(action['domain']) action['domain'].append(('partner_id', 'child_of', self.id)) return action + + @api.onchange('company_id') + def _onchange_company_id(self): + company = self.env['res.company'] + if self.company_id: + company = self.company_id + else: + company = self.env.user.company_id + return {'domain': {'property_account_position_id': [('company_id', 'in', [company.id, False])]}} diff --git a/addons/account/models/product.py b/addons/account/models/product.py index a5a6afb5..d57b836e 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -35,22 +35,6 @@ class ProductTemplate(models.Model): domain=[('deprecated', '=', False)], help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation. If the field is empty, it uses the one defined in the product category.") - @api.multi - def write(self, vals): - #TODO: really? i don't see the reason we'd need that constraint.. - check = self.ids and 'uom_po_id' in vals - if check: - self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)]) - uoms = dict(self._cr.fetchall()) - res = super(ProductTemplate, self).write(vals) - if check: - self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)]) - if dict(self._cr.fetchall()) != uoms: - products = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids)]) - if self.env['account.move.line'].search_count([('product_id', 'in', products.ids)]): - raise UserError(_('You can not change the unit of measure of a product that has been already used in an account journal item. If you need to change the unit of measure, you may deactivate this product.')) - return res - @api.multi def _get_product_accounts(self): return { diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index d7ce4aac..f8c30b32 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -193,7 +193,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): values['name'] = _('Unknown Partner') values['trust'] = False - if at_least_one_amount or self._context.get('include_nullified_amount'): + if at_least_one_amount or (self._context.get('include_nullified_amount') and lines[partner['partner_id']]): res.append(values) return res, total, lines diff --git a/addons/account/report/account_general_ledger.py b/addons/account/report/account_general_ledger.py index 87b7a417..4649e0e7 100644 --- a/addons/account/report/account_general_ledger.py +++ b/addons/account/report/account_general_ledger.py @@ -38,7 +38,7 @@ class ReportGeneralLedger(models.AbstractModel): init_wheres.append(init_where_clause.strip()) init_filters = " AND ".join(init_wheres) filters = init_filters.replace('account_move_line__move_id', 'm').replace('account_move_line', 'l') - sql = ("""SELECT 0 AS lid, l.account_id AS account_id, '' AS ldate, '' AS lcode, NULL AS amount_currency, '' AS lref, 'Initial Balance' AS lname, COALESCE(SUM(l.debit),0.0) AS debit, COALESCE(SUM(l.credit),0.0) AS credit, COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance, '' AS lpartner_id,\ + sql = ("""SELECT 0 AS lid, l.account_id AS account_id, '' AS ldate, '' AS lcode, 0.0 AS amount_currency, '' AS lref, 'Initial Balance' AS lname, COALESCE(SUM(l.debit),0.0) AS debit, COALESCE(SUM(l.credit),0.0) AS credit, COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance, '' AS lpartner_id,\ '' AS move_name, '' AS mmove_id, '' AS currency_code,\ NULL AS currency_id,\ '' AS invoice_id, '' AS invoice_type, '' AS invoice_number,\ diff --git a/addons/account/report/account_overdue_report.py b/addons/account/report/account_overdue_report.py index d31d1f32..0e95fba0 100644 --- a/addons/account/report/account_overdue_report.py +++ b/addons/account/report/account_overdue_report.py @@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel): "FROM account_move_line l " "JOIN account_account_type at ON (l.user_type_id = at.id) " "JOIN account_move m ON (l.move_id = m.id) " - "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) + "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) for row in self.env.cr.dictfetchall(): res[row.pop('partner_id')].append(row) return res diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index c7fd2779..be6f467c 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -784,6 +784,7 @@ var StatementModel = BasicModel.extend({ model: 'account.tax', method: 'json_friendly_compute_all', args: args, + context: $.extend(self.context || {}, {'round': true}), }) .then(function (result) { _.each(result.taxes, function(tax){ @@ -847,7 +848,7 @@ var StatementModel = BasicModel.extend({ }) : false, account_code: self.accounts[line.st_line.open_balance_account_id], }; - line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1; + line.balance.type = line.balance.amount_currency ? (line.st_line.partner_id ? 0 : -1) : 1; }); }, /** @@ -963,6 +964,7 @@ var StatementModel = BasicModel.extend({ var formatOptions = { currency_id: line.st_line.currency_id, }; + var amount = values.amount !== undefined ? values.amount : line.balance.amount; var prop = { 'id': _.uniqueId('createLine'), 'label': values.label || line.st_line.name, @@ -974,8 +976,7 @@ var StatementModel = BasicModel.extend({ 'debit': 0, 'credit': 0, 'base_amount': values.amount_type !== "percentage" ? - (values.amount || line.balance.amount) : - line.balance.amount * values.amount / 100, + (amount) : line.balance.amount * values.amount / 100, 'percent': values.amount_type === "percentage" ? values.amount : null, 'link': values.link, 'display': true, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 58ee890f..1f70529a 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -298,7 +298,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { }; self.fields.partner_id.appendTo(self.$('.accounting_view caption')); }); - this.$('thead .line_info_button').attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState})); + $('') + .appendTo(this.$('thead .cell_info_popover')) + .attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState})); this.$el.popover({ 'selector': '.line_info_button', 'placement': 'left', @@ -626,7 +628,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { /** * @private */ - _onFilterChange: function () { + _onFilterChange: function (event) { this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())}); }, /** diff --git a/addons/account/static/src/less/account_reconciliation.less b/addons/account/static/src/less/account_reconciliation.less index deb0c76c..f15e82e6 100644 --- a/addons/account/static/src/less/account_reconciliation.less +++ b/addons/account/static/src/less/account_reconciliation.less @@ -159,6 +159,9 @@ } /* info popover */ + .popover { + max-width: none; + } table.details { vertical-align: top; @@ -209,6 +212,7 @@ } &[data-mode="match"] > .match { max-height: none; + overflow: visible; .o-transition(max-height, 400ms); } &[data-mode="create"] > .create { diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 7d24458f..897b7a0c 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -128,7 +128,7 @@

- + @@ -251,7 +251,7 @@ - +
Create Write-offOpen balanceChoose counterpartOpen balanceChoose counterpart or Create Write-off
Description
Amount ()
Account
Note
Note
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 0cbb4f9a..d23fc8e1 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -1019,8 +1019,8 @@ QUnit.module('account', { widget.$('.create .create_label input').val('test1').trigger('input'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Create Write-off' with 25.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Open balance' with 25.00 in left column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines"); clientAction.destroy(); @@ -1126,8 +1126,8 @@ QUnit.module('account', { $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Open balance' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); clientAction.destroy(); @@ -1151,7 +1151,7 @@ QUnit.module('account', { assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), - "Create Write-off229.12", "should display the 'Create Write-off' line with value in left column"); + "Open balance229.12", "should display the 'Open balance' line with value in left column"); widget.$('.create .create_amount input').val('100').trigger('input'); @@ -1159,7 +1159,7 @@ QUnit.module('account', { " 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ", "should update the value of the 4 lines (because the line must have 100% of the value)"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), - "Create Write-off215.00", "should change the 'Create Write-off' line because the 20.00% tax is not an include tax"); + "Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax"); widget.$('.accounting_view tbody .cell_account_code:first').trigger('click'); widget.$('.accounting_view tbody .cell_label:first').trigger('click'); diff --git a/addons/account/tests/test_account_move_closed_period.py b/addons/account/tests/test_account_move_closed_period.py index 1ed6013c..d95fd1e3 100644 --- a/addons/account/tests/test_account_move_closed_period.py +++ b/addons/account/tests/test_account_move_closed_period.py @@ -1,6 +1,8 @@ from flectra.addons.account.tests.account_test_classes import AccountingTestCase from flectra.osv.orm import except_orm -from datetime import datetime, timedelta +from datetime import datetime +from dateutil.relativedelta import relativedelta +from calendar import monthrange from flectra.tools import DEFAULT_SERVER_DATE_FORMAT class TestPeriodState(AccountingTestCase): @@ -11,14 +13,16 @@ class TestPeriodState(AccountingTestCase): def setUp(self): super(TestPeriodState, self).setUp() self.user_id = self.env.user - self.day_before_yesterday = datetime.now() - timedelta(2) - self.yesterday = datetime.now() - timedelta(1) - self.yesterday_str = self.yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) + + last_day_month = datetime.now() - relativedelta(months=1) + last_day_month = last_day_month.replace(day=monthrange(last_day_month.year, last_day_month.month)[1]) + self.last_day_month_str = last_day_month.strftime(DEFAULT_SERVER_DATE_FORMAT) + #make sure there is no unposted entry - draft_entries = self.env['account.move'].search([('date', '<=', self.yesterday_str), ('state', '=', 'draft')]) + draft_entries = self.env['account.move'].search([('date', '<=', self.last_day_month_str), ('state', '=', 'draft')]) if draft_entries: draft_entries.post() - self.user_id.company_id.write({'fiscalyear_lock_date': self.yesterday_str}) + self.user_id.company_id.fiscalyear_lock_date = self.last_day_month_str self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale')])[0] self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable')])[0] @@ -27,7 +31,7 @@ class TestPeriodState(AccountingTestCase): move = self.env['account.move'].create({ 'name': '/', 'journal_id': self.sale_journal_id.id, - 'date': self.day_before_yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'date': self.last_day_month_str, 'line_ids': [(0, 0, { 'name': 'foo', 'debit': 10, diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index aeacd3e6..8f7421d2 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -40,6 +40,12 @@ class TestReconciliation(AccountingTestCase): self.diff_income_account = self.env['res.users'].browse(self.env.uid).company_id.income_currency_exchange_account_id self.diff_expense_account = self.env['res.users'].browse(self.env.uid).company_id.expense_currency_exchange_account_id + self.inbound_payment_method = self.env['account.payment.method'].create({ + 'name': 'inbound', + 'code': 'IN', + 'payment_type': 'inbound', + }) + def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None): #we create an invoice in given currency invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id, @@ -657,3 +663,151 @@ class TestReconciliation(AccountingTestCase): credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile() self.assertAlmostEquals(inv1.residual, 10) self.assertAlmostEquals(inv2.residual, 20) + + def test_unreconcile_exchange(self): + # Use case: + # - Company currency in EUR + # - Create 2 rates for USD: + # 1.0 on 2018-01-01 + # 0.5 on 2018-02-01 + # - Create an invoice on 2018-01-02 of 111 USD + # - Register a payment on 2018-02-02 of 111 USD + # - Unreconcile the payment + + self.env['res.currency.rate'].create({ + 'name': time.strftime('%Y') + '-07-01', + 'rate': 1.0, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id + }) + self.env['res.currency.rate'].create({ + 'name': time.strftime('%Y') + '-08-01', + 'rate': 0.5, + 'currency_id': self.currency_usd_id, + 'company_id': self.env.ref('base.main_company').id + }) + inv = self.create_invoice(invoice_amount=111, currency_id=self.currency_usd_id) + payment = self.env['account.payment'].create({ + 'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'amount': 111, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_usd.id, + 'payment_date': time.strftime('%Y') + '-08-01', + }) + payment.post() + credit_aml = payment.move_line_ids.filtered('credit') + + # Check residual before assignation + self.assertAlmostEquals(inv.residual, 111) + + # Assign credit, check exchange move and residual + inv.assign_outstanding_credit(credit_aml.id) + self.assertEqual(len(payment.move_line_ids.mapped('full_reconcile_id').exchange_move_id), 1) + self.assertAlmostEquals(inv.residual, 0) + + # Unreconcile invoice and check residual + credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile() + self.assertAlmostEquals(inv.residual, 111) + + def test_revert_payment_and_reconcile(self): + payment = self.env['account.payment'].create({ + 'payment_method_id': self.inbound_payment_method.id, + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'journal_id': self.bank_journal_usd.id, + 'payment_date': '2018-06-04', + 'amount': 666, + }) + payment.post() + + self.assertEqual(len(payment.move_line_ids), 2) + + bank_line = payment.move_line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id) + customer_line = payment.move_line_ids - bank_line + + self.assertEqual(len(bank_line), 1) + self.assertEqual(len(customer_line), 1) + self.assertNotEqual(bank_line.id, customer_line.id) + + self.assertEqual(bank_line.move_id.id, customer_line.move_id.id) + move = bank_line.move_id + + # Reversing the payment's move + reversed_move_list = move.reverse_moves('2018-06-04') + self.assertEqual(len(reversed_move_list), 1) + reversed_move = self.env['account.move'].browse(reversed_move_list[0]) + + self.assertEqual(len(reversed_move.line_ids), 2) + + # Testing the reconciliation matching between the move lines and their reversed counterparts + reversed_bank_line = reversed_move.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id) + reversed_customer_line = reversed_move.line_ids - reversed_bank_line + + self.assertEqual(len(reversed_bank_line), 1) + self.assertEqual(len(reversed_customer_line), 1) + self.assertNotEqual(reversed_bank_line.id, reversed_customer_line.id) + self.assertEqual(reversed_bank_line.move_id.id, reversed_customer_line.move_id.id) + + self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id) + self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id) + + def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False): + #we create an invoice in given currency + invoice = self.account_invoice_model.create({'partner_id': partner_id, + 'reference_type': 'none', + 'currency_id': currency_id, + 'name': type == 'out_invoice' and 'invoice to client' or 'invoice to vendor', + 'account_id': self.account_rcv.id, + 'type': type, + 'date_invoice': time.strftime('%Y') + '-07-01', + }) + self.account_invoice_line_model.create({'product_id': self.product.id, + 'quantity': 1, + 'price_unit': invoice_amount, + 'invoice_id': invoice.id, + 'name': 'product that cost ' + str(invoice_amount), + 'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + }) + + #validate invoice + invoice.action_invoice_open() + return invoice + + def test_aged_report(self): + AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True) + account_type = ['receivable'] + report_date_to = time.strftime('%Y') + '-07-15' + partner = self.env['res.partner'].create({'name': 'AgedPartner'}) + currency = self.env.user.company_id.currency_id + + invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id) + journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'currency_id': currency.id}) + + statement = self.make_payment(invoice, journal, 50) + + # Case 1: The invoice and payment are reconciled: Nothing should appear + report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30) + + partner_lines = [line for line in report_lines if line['partner_id'] == partner.id] + self.assertEqual(partner_lines, [], 'The aged receivable shouldn\'t have lines at this point') + self.assertFalse(partner.id in amls, 'The aged receivable should not have amls either') + + # Case 2: The invoice and payment are not reconciled: we should have one line on the report + # and 2 amls + invoice.move_id.line_ids.with_context(invoice_id=invoice.id).remove_move_reconcile() + report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30) + + partner_lines = [line for line in report_lines if line['partner_id'] == partner.id] + self.assertEqual(partner_lines, [{'trust': 'normal', '1': 0.0, '0': 0.0, 'direction': 0.0, 'partner_id': partner.id, '3': 0.0, 'total': 0.0, 'name': 'AgedPartner', '4': 0.0, '2': 0.0}], + 'We should have a line in the report for the partner') + self.assertEqual(len(amls[partner.id]), 2, 'We should have 2 account move lines for the partner') + + positive_line = [line for line in amls[partner.id] if line['line'].balance > 0] + negative_line = [line for line in amls[partner.id] if line['line'].balance < 0] + + self.assertEqual(positive_line[0]['amount'], 50.0, 'The amount of the amls should be 50') + self.assertEqual(negative_line[0]['amount'], -50.0, 'The amount of the amls should be -50') diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index c9521e24..66085a62 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -73,7 +73,7 @@ - + diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index e2e88937..04c70995 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -248,14 +248,16 @@
-
-
- Latest Statement + +
+
+ Latest Statement +
+
+ +
-
- -
-
+
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index fc34fcc0..9011c78e 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -146,8 +146,8 @@ - - + + @@ -1475,7 +1476,7 @@ - + diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml index 6ea9fe8d..fe9f61f2 100644 --- a/addons/account/views/partner_view.xml +++ b/addons/account/views/partner_view.xml @@ -95,6 +95,7 @@ + @@ -111,7 +112,7 @@
+
+
diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py index 0699cdbb..0db77065 100644 --- a/addons/account_analytic_default/models/account_analytic_default.py +++ b/addons/account_analytic_default/models/account_analytic_default.py @@ -68,7 +68,7 @@ class AccountInvoiceLine(models.Model): if not self.account_analytic_id: rec = self.env['account.analytic.default'].account_get( self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid, - fields.Date.today(), company_id=self.company_id.id) + fields.Date.today(), company_id=invoice.company_id.id) if rec: self.account_analytic_id = rec.analytic_id.id super(AccountInvoiceLine, self)._set_additional_fields(invoice) diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py index 1cfc36f3..1575e4c9 100644 --- a/addons/account_payment/controllers/payment.py +++ b/addons/account_payment/controllers/payment.py @@ -17,7 +17,6 @@ class PaymentPortal(http.Controller): :return html: form containing all values related to the acquirer to redirect customers to the acquirer website """ success_url = kwargs.get('success_url', '/my') - callback_method = kwargs.get('callback_method', '') invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id) if not invoice_sudo: @@ -28,17 +27,16 @@ class PaymentPortal(http.Controller): except: return False + if request.env.user == request.env.ref('base.public_user'): + save_token = False # we avoid to create a token for the public user + token = request.env['payment.token'].sudo() # currently no support of payment tokens tx = request.env['payment.transaction'].sudo()._check_or_create_invoice_tx( invoice_sudo, acquirer, payment_token=token, tx_type='form_save' if save_token else 'form', - add_tx_values={ - 'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id, - 'callback_res_id': invoice_sudo.id, - 'callback_method': callback_method, - }) + ) # set the transaction id into the session request.session['portal_invoice_%s_transaction_id' % invoice_sudo.id] = tx.id @@ -58,7 +56,6 @@ class PaymentPortal(http.Controller): """ Use a token to perform a s2s transaction """ error_url = kwargs.get('error_url', '/my') success_url = kwargs.get('success_url', '/my') - callback_method = kwargs.get('callback_method', '') access_token = kwargs.get('access_token') params = {} if access_token: @@ -73,7 +70,8 @@ class PaymentPortal(http.Controller): token = request.env['payment.token'].sudo().browse(int(pm_id)) except (ValueError, TypeError): token = False - if not token: + token_owner = invoice_sudo.partner_id if request.env.user == request.env.ref('base.public_user') else request.env.user.partner_id + if not token or token.partner_id != token_owner: params['error'] = 'pay_invoice_invalid_token' return request.redirect(_build_url_w_params(error_url, params)) @@ -83,11 +81,7 @@ class PaymentPortal(http.Controller): token.acquirer_id, payment_token=token, tx_type='server2server', - add_tx_values={ - 'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', invoice_sudo._name)], limit=1).id, - 'callback_res_id': invoice_sudo.id, - 'callback_method': callback_method, - }) + ) # set the transaction id into the session request.session['portal_invoice_%s_transaction_id' % invoice_sudo.id] = tx.id diff --git a/addons/account_payment/controllers/portal.py b/addons/account_payment/controllers/portal.py index e2feed24..1a358b6b 100644 --- a/addons/account_payment/controllers/portal.py +++ b/addons/account_payment/controllers/portal.py @@ -9,5 +9,18 @@ class PortalAccount(PortalAccount): def _invoice_get_page_view_values(self, invoice, access_token, **kwargs): values = super(PortalAccount, self)._invoice_get_page_view_values(invoice, access_token, **kwargs) - values.update(request.env['payment.acquirer']._get_available_payment_input(invoice.partner_id, invoice.company_id)) + payment_inputs = request.env['payment.acquirer']._get_available_payment_input(company=invoice.company_id) + # if not connected (using public user), the method _get_available_payment_input will return public user tokens + is_public_user = request.env.ref('base.public_user') == request.env.user + if is_public_user: + # we should not display payment tokens owned by the public user + payment_inputs.pop('pms', None) + token_count = request.env['payment.token'].sudo().search_count([('acquirer_id.company_id', '=', invoice.company_id.id), + ('partner_id', '=', invoice.partner_id.id), + ]) + values['existing_token'] = token_count > 0 + values.update(payment_inputs) + # if the current user is connected we set partner_id to his partner otherwise we set it as the invoice partner + # we do this to force the creation of payment tokens to the correct partner and avoid token linked to the public user + values['partner_id'] = invoice.partner_id if is_public_user else request.env.user.partner_id, return values diff --git a/addons/account_payment/models/payment.py b/addons/account_payment/models/payment.py index 30b8f0b0..39a79004 100644 --- a/addons/account_payment/models/payment.py +++ b/addons/account_payment/models/payment.py @@ -159,7 +159,7 @@ class PaymentTransaction(models.Model): 'currency_id': invoice.currency_id.id, 'partner_id': invoice.partner_id.id, 'partner_country_id': invoice.partner_id.country_id.id, - 'reference': self.get_next_reference(invoice.number), + 'reference': self._get_next_reference(invoice.number, acquirer=acquirer), 'account_invoice_id': invoice.id, } if add_tx_values: diff --git a/addons/account_payment/views/account_portal_templates.xml b/addons/account_payment/views/account_portal_templates.xml index d2ded1c5..87b5f210 100644 --- a/addons/account_payment/views/account_portal_templates.xml +++ b/addons/account_payment/views/account_portal_templates.xml @@ -5,7 +5,7 @@ - + @@ -83,6 +83,11 @@
+
+
+ You have credits card registered, you can log-in to be able to use them. +
+
@@ -156,7 +161,8 @@ inherit_id="account.portal_invoice_success"> - Invoice successfully paid. + + diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 0bbeb912..80b4bdc7 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -154,11 +154,6 @@ class AccountVoucher(models.Model): voucher.amount = total + voucher.tax_correction voucher.tax_amount = tax_amount - @api.one - @api.depends('account_pay_now_id', 'account_pay_later_id', 'pay_now') - def _get_account(self): - self.account_id = self.account_pay_now_id if self.pay_now == 'pay_now' else self.account_pay_later_id - @api.onchange('date') def onchange_date(self): self.account_date = self.date @@ -315,6 +310,9 @@ class AccountVoucher(models.Model): #create one move line per voucher line where amount is not 0.0 if not line.price_subtotal: continue + line_subtotal = line.price_subtotal + if self.voucher_type == 'sale': + line_subtotal = -1 * line.price_subtotal # convert the amount set on the voucher line into the currency of the voucher's company # this calls res_curreny.compute() with the right context, # so that it will take either the rate on the voucher if it is relevant or will use the default behaviour @@ -331,7 +329,7 @@ class AccountVoucher(models.Model): 'debit': abs(amount) if self.voucher_type == 'purchase' else 0.0, 'date': self.account_date, 'tax_ids': [(4,t.id) for t in line.tax_ids], - 'amount_currency': line.price_subtotal if current_currency != company_currency else 0.0, + 'amount_currency': line_subtotal if current_currency != company_currency else 0.0, 'currency_id': company_currency != current_currency and current_currency or False, 'payment_id': self._context.get('payment_id'), } diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py index d3c65f4e..4e00d792 100644 --- a/addons/analytic/models/analytic_account.py +++ b/addons/analytic/models/analytic_account.py @@ -21,21 +21,24 @@ class AccountAnalyticAccount(models.Model): @api.multi def _compute_debit_credit_balance(self): analytic_line_obj = self.env['account.analytic.line'] - domain = [('account_id', 'in', self.mapped('id'))] + domain = [('account_id', 'in', self.ids)] if self._context.get('from_date', False): domain.append(('date', '>=', self._context['from_date'])) if self._context.get('to_date', False): domain.append(('date', '<=', self._context['to_date'])) - account_amounts = analytic_line_obj.search_read(domain, ['account_id', 'amount']) - account_ids = set([line['account_id'][0] for line in account_amounts]) - data_debit = {account_id: 0.0 for account_id in account_ids} - data_credit = {account_id: 0.0 for account_id in account_ids} - for account_amount in account_amounts: - if account_amount['amount'] < 0.0: - data_debit[account_amount['account_id'][0]] += account_amount['amount'] - else: - data_credit[account_amount['account_id'][0]] += account_amount['amount'] + credit_groups = analytic_line_obj.read_group( + domain=domain + [('amount', '>=', 0.0)], + fields=['account_id', 'amount'], + groupby=['account_id'] + ) + data_credit = {l['account_id'][0]: l['amount'] for l in credit_groups} + debit_groups = analytic_line_obj.read_group( + domain=domain + [('amount', '<', 0.0)], + fields=['account_id', 'amount'], + groupby=['account_id'] + ) + data_debit = {l['account_id'][0]: l['amount'] for l in debit_groups} for account in self: account.debit = abs(data_debit.get(account.id, 0.0)) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index d3323f8d..5e4f6076 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -4,7 +4,7 @@ import base64 import babel.dates import collections -from datetime import datetime, timedelta +from datetime import datetime, timedelta, MAXYEAR from dateutil import parser from dateutil import rrule from dateutil.relativedelta import relativedelta @@ -608,7 +608,8 @@ class Meeting(models.Model): if not event_date: event_date = datetime.now() - if self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule: + use_naive_datetime = self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule + if use_naive_datetime: rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True) else: # Convert the event date to saved timezone (or context tz) as it'll @@ -617,9 +618,21 @@ class Meeting(models.Model): rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={}) recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)]) - for meeting in recurring_meetings: - rset1._exdate.append(todate(meeting.recurrent_id_date)) - return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1] + # We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to + # control the memory usage. + invalidate = False + for meetings in self.env.cr.split_for_in_conditions(recurring_meetings, size=50000): + if invalidate: + self.invalidate_cache() + for meeting in meetings: + recurring_date = fields.Datetime.from_string(meeting.recurrent_id_date) + if use_naive_datetime: + recurring_date = recurring_date.replace(tzinfo=None) + else: + recurring_date = todate(meeting.recurrent_id_date) + rset1.exdate(recurring_date) + invalidate = True + return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR] @api.multi def _get_recurrency_end_date(self): @@ -642,7 +655,13 @@ class Meeting(models.Model): }[data['rrule_type']] deadline = fields.Datetime.from_string(data['stop']) - return deadline + relativedelta(**{delay: count * mult}) + computed_final_date = False + while not computed_final_date and count > 0: + try: # may crash if year > 9999 (in case of recurring events) + computed_final_date = deadline + relativedelta(**{delay: count * mult}) + except ValueError: + count -= data['interval'] + return computed_final_date or deadline return final_date @api.multi @@ -883,8 +902,8 @@ class Meeting(models.Model): startdate = startdate.astimezone(pytz.utc) # Convert to UTC meeting.start = fields.Datetime.to_string(startdate) else: - meeting.start = meeting.start_datetime - meeting.stop = meeting.stop_datetime + meeting.write({'start': meeting.start_datetime, + 'stop': meeting.stop_datetime}) @api.depends('byday', 'recurrency', 'final_date', 'rrule_type', 'month_by', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'week_list') def _compute_rrule(self): @@ -910,9 +929,13 @@ class Meeting(models.Model): def _check_closing_date(self): for meeting in self: if meeting.start_datetime and meeting.stop_datetime and meeting.stop_datetime < meeting.start_datetime: - raise ValidationError(_('Ending datetime cannot be set before starting datetime.')) + raise ValidationError(_('Ending datetime cannot be set before starting datetime.') + "\n" + + _("Meeting '%s' starts '%s' and ends '%s'") % (meeting.name, meeting.start_datetime, meeting.stop_datetime) + ) if meeting.start_date and meeting.stop_date and meeting.stop_date < meeting.start_date: - raise ValidationError(_('Ending date cannot be set before starting date.')) + raise ValidationError(_('Ending date cannot be set before starting date.') + "\n" + + _("Meeting '%s' starts '%s' and ends '%s'") % (meeting.name, meeting.start_date, meeting.stop_date) + ) @api.onchange('start_datetime', 'duration') def _onchange_duration(self): @@ -1141,9 +1164,15 @@ class Meeting(models.Model): for key in (order or self._order).split(',') )) def key(record): + # we need to deal with undefined fields, as sorted requires an homogeneous iterable + def boolean_product(x): + x = False if (isinstance(x, models.Model) and not x) else x + if isinstance(x, bool): + return (x, x) + return (True, x) # first extract the values for each key column (ids need special treatment) vals_spec = ( - (any_id2key(record[name]) if name == 'id' else record[name], desc) + (any_id2key(record[name]) if name == 'id' else boolean_product(record[name]), desc) for name, desc in sort_spec ) # then Reverse if the value matches a "desc" column @@ -1218,7 +1247,12 @@ class Meeting(models.Model): def _rrule_parse(self, rule_str, data, date_start): day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'] rrule_type = ['yearly', 'monthly', 'weekly', 'daily'] - rule = rrule.rrulestr(rule_str, dtstart=fields.Datetime.from_string(date_start)) + ddate = fields.Datetime.from_string(date_start) + if 'Z' in rule_str and not ddate.tzinfo: + ddate = ddate.replace(tzinfo=pytz.timezone('UTC')) + rule = rrule.rrulestr(rule_str, dtstart=ddate) + else: + rule = rrule.rrulestr(rule_str, dtstart=ddate) if rule._freq > 0 and rule._freq < 4: data['rrule_type'] = rrule_type[rule._freq] diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml index ee55631a..ad937bbf 100644 --- a/addons/calendar/views/calendar_views.xml +++ b/addons/calendar/views/calendar_views.xml @@ -194,7 +194,6 @@ - diff --git a/addons/crm/__init__.py b/addons/crm/__init__.py index 968c635d..108d85ce 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -5,3 +5,11 @@ from . import controllers from . import models from . import report from . import wizard + +from flectra import api, SUPERUSER_ID + + +def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + teams = env['crm.team'].search([('dashboard_graph_model', '=', 'crm.opportunity.report')]) + teams.update({'dashboard_graph_model': None}) diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 4204bd98..2d8f2c42 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -58,4 +58,5 @@ 'installable': True, 'application': True, 'auto_install': False, + 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 5f135a53..07959833 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -236,6 +236,8 @@ class Lead(models.Model): @api.model def _onchange_user_values(self, user_id): """ returns new values when user_id has changed """ + if not user_id: + return {} if user_id and self._context.get('team_id'): team = self.env['crm.team'].browse(self._context['team_id']) if user_id in team.member_ids.ids: @@ -440,7 +442,8 @@ class Lead(models.Model): 'res_id': self.id, 'views': [(form_view.id, 'form'),], 'type': 'ir.actions.act_window', - 'target': 'inline' + 'target': 'inline', + 'context': {'default_type': 'opportunity'} } # ---------------------------------------- diff --git a/addons/crm/static/src/js/tour.js b/addons/crm/static/src/js/tour.js index 28d5b17d..78070fe4 100644 --- a/addons/crm/static/src/js/tour.js +++ b/addons/crm/static/src/js/tour.js @@ -58,7 +58,7 @@ tour.register('crm_tour', { }, { trigger: ".o_opportunity_form .o_chatter_button_new_message", content: _t('

Send messages to your prospect and get replies automatically attached to this opportunity.

Type \'@\' to mention people - it\'s like cc-ing on emails.

'), - position: "top" + position: "bottom" }, { trigger: ".breadcrumb li:not(.active):last", extra_trigger: '.o_opportunity_form', diff --git a/addons/crm/wizard/base_partner_merge.py b/addons/crm/wizard/base_partner_merge.py index 08f1f8c0..2d2b0396 100644 --- a/addons/crm/wizard/base_partner_merge.py +++ b/addons/crm/wizard/base_partner_merge.py @@ -223,7 +223,7 @@ class MergePartnerAutomatic(models.TransientModel): """ _logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids) - model_fields = dst_partner._fields + model_fields = dst_partner.fields_get().keys() def write_serializer(item): if isinstance(item, models.BaseModel): @@ -232,7 +232,8 @@ class MergePartnerAutomatic(models.TransientModel): return item # get all fields that are not computed or x2many values = dict() - for column, field in model_fields.items(): + for column in model_fields: + field = dst_partner._fields[column] if field.type not in ('many2many', 'one2many') and field.compute is None: for item in itertools.chain(src_partners, [dst_partner]): if item[column]: diff --git a/addons/crm/wizard/crm_lead_to_opportunity_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_views.xml index 5e34f90c..2acfd63e 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_views.xml +++ b/addons/crm/wizard/crm_lead_to_opportunity_views.xml @@ -56,8 +56,7 @@ - - +