# Copyright 2017 Akretion (Alexis de Lattre ) # Copyright 2018-2022 Camptocamp # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields, api, _ from odoo.exceptions import UserError import odoo.addons.decimal_precision as dp class AccountMoveUpdate(models.TransientModel): _name = 'account.move.update' _description = 'Wizard to update non-legal fields of invoice' invoice_id = fields.Many2one( 'account.move', string='Invoice', required=True, readonly=True) move_type = fields.Selection(related='invoice_id.move_type') company_id = fields.Many2one(related='invoice_id.company_id') partner_id = fields.Many2one(related='invoice_id.partner_id') user_id = fields.Many2one('res.users', string='Salesperson') invoice_payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Term') ref = fields.Char(string='Reference') # field label is customized in the view invoice_date = fields.Date() invoice_origin = fields.Char(string='Source Document') partner_bank_id = fields.Many2one( 'res.partner.bank', string='Bank Account') line_ids = fields.One2many( 'account.move.line.update', 'parent_id', string='Invoice Lines') @api.model def _simple_fields2update(self): '''List boolean, date, datetime, char, text fields''' return ['ref', 'invoice_origin', 'invoice_date'] @api.model def _m2o_fields2update(self): return ['invoice_payment_term_id', 'user_id', 'partner_bank_id'] @api.model def _prepare_default_get(self, invoice): res = {'invoice_id': invoice.id, 'line_ids': []} for sfield in self._simple_fields2update(): res[sfield] = invoice[sfield] for m2ofield in self._m2o_fields2update(): res[m2ofield] = invoice[m2ofield].id or False for line in invoice.invoice_line_ids: aa_tags = line.analytic_tag_ids aa_tags = [(6, 0, aa_tags.ids)] if aa_tags else False res['line_ids'].append([0, 0, { 'invoice_line_id': line.id, 'sequence': line.sequence, 'name': line.name, 'quantity': line.quantity, 'price_subtotal': line.price_subtotal, 'analytic_account_id': line.analytic_account_id.id, 'currency_id': line.currency_id.id, 'analytic_tag_ids': aa_tags, 'display_type': line.display_type, }]) return res @api.onchange('move_type') def move_type_on_change(self): res = {'domain': {}} if self.move_type in ('out_invoice', 'out_refund'): res['domain']['partner_bank_id'] =\ "[('partner_id.ref_company_ids', 'in', [company_id])]" else: res['domain']['partner_bank_id'] =\ "[('partner_id', '=', partner_id)]" return res def _prepare_invoice(self): vals = {} inv = self.invoice_id for sfield in self._simple_fields2update(): if self[sfield] != inv[sfield]: vals[sfield] = self[sfield] for m2ofield in self._m2o_fields2update(): if self[m2ofield] != inv[m2ofield]: vals[m2ofield] = self[m2ofield].id or False if 'invoice_payment_term_id' in vals: pterm_list = self.invoice_payment_term_id.compute( value=1, date_ref=inv.date_invoice)[0] if pterm_list: vals['date_due'] = max(line[0] for line in pterm_list) return vals @api.model def _line_simple_fields2update(self): return ["name"] @api.model def _line_m2o_fields2update(self): return ["analytic_account_id"] @api.model def _line_m2m_fields2update(self): return ["analytic_tag_ids"] @api.model def _prepare_invoice_line(self, line): vals = {} for field in self._line_simple_fields2update(): if line[field] != line.invoice_line_id[field]: vals[field] = line[field] for field in self._line_m2o_fields2update(): if line[field] != line.invoice_line_id[field]: vals[field] = line[field].id for field in self._line_m2m_fields2update(): if line[field] != line.invoice_line_id[field]: vals[field] = [(6, 0, line[field].ids)] return vals def _prepare_move_line_and_analytic_line(self, inv_line): mlvals = {} alvals = {} inv_line_upd = self.line_ids.filtered( lambda rec: rec.invoice_line_id == inv_line) ini_aa = inv_line.analytic_account_id new_aa = inv_line_upd.analytic_account_id if ini_aa != new_aa: mlvals['analytic_account_id'] = new_aa.id alvals['account_id'] = new_aa.id ini_aa_tags = inv_line.analytic_tag_ids new_aa_tags = inv_line_upd.analytic_tag_ids if ini_aa_tags != new_aa_tags: mlvals['analytic_tag_ids'] = [(6, None, new_aa_tags.ids)] alvals['tag_ids'] = [(6, None, new_aa_tags.ids)] return mlvals, alvals def _update_payment_term_move(self): self.ensure_one() inv = self.invoice_id if ( self.invoice_payment_term_id and self.invoice_payment_term_id != inv.invoice_payment_term_id): # I don't update pay term when the invoice is partially (or fully) # paid because if you have a payment term with several lines # of the same amount, you would also have to take into account # the reconcile marks to put the new maturity date on the right # lines if inv.payment_id: raise UserError(_( "This wizard doesn't support the update of payment " "terms on an invoice which is partially or fully " "paid.")) prec = self.env['decimal.precision'].precision_get('Account') term_res = self.invoice_payment_term_id.compute( inv.amount_total, inv.date_invoice)[0] new_pterm = {} # key = int(amount * 100), value = [date1, date2] for entry in term_res: amount = int(entry[1] * 10 * prec) if amount in new_pterm: new_pterm[amount].append(entry[0]) else: new_pterm[amount] = [entry[0]] mlines = {} # key = int(amount * 100), value : [line1, line2] for line in inv.move_id.line_ids: if line.account_id == inv.account_id: amount = int(abs(line.credit - line.debit) * 10 * prec) if amount in mlines: mlines[amount].append(line) else: mlines[amount] = [line] for iamount, lines in mlines.items(): if len(lines) != len(new_pterm.get(iamount, [])): raise UserError(_( "The original payment term '%s' doesn't have the " "same terms (number of terms and/or amount) as the " "new payment term '%s'. You can only switch to a " "payment term that has the same number of terms " "with the same amount.") % ( inv.invoice_payment_term_id.name, self.invoice_payment_term_id.name)) for line in lines: line.date_maturity = new_pterm[iamount].pop() def run(self): self.ensure_one() inv = self.invoice_id updated = False # re-write date_maturity on move line self._update_payment_term_move() ivals = self._prepare_invoice() if ivals: updated = True inv.write(ivals) if inv: for ml in inv.line_ids.filtered( # we are only interested in invoice lines, not tax lines lambda rec: bool(rec.product_id) ): if ml.credit == 0.0: continue analytic_account = ml.analytic_account_id mlvals, alvals = self._prepare_move_line_and_analytic_line(ml) if mlvals: updated = True ml.write(mlvals) aalines = ml.analytic_line_ids if aalines and alvals: updated = True if ('account_id' in alvals and alvals['account_id'] is False): former_aa = analytic_account to_remove_aalines = aalines.filtered( lambda rec: rec.account_id == former_aa) # remove existing analytic line to_remove_aalines.unlink() else: aalines.write(alvals) elif 'account_id' in alvals: # Create analytic lines if analytic account # is added later ml.create_analytic_lines() for line in self.line_ids: ilvals = self._prepare_invoice_line(line) if ilvals: updated = True line.invoice_line_id.write(ilvals) if updated: inv.message_post(body=_( 'Non-legal fields of invoice updated via the Invoice Update ' 'wizard.')) # Purge existing PDF report = self.env.ref("account.account_invoices") attachment = report.retrieve_attachment(inv) # attachment may be None if attachment: attachment.unlink() return True class AccountMoveLineUpdate(models.TransientModel): _name = 'account.move.line.update' _description = 'Update non-legal fields of invoice lines' _order = "sequence, name" sequence = fields.Integer() parent_id = fields.Many2one( 'account.move.update', string='Wizard', ondelete='cascade') invoice_line_id = fields.Many2one( 'account.move.line', string='Invoice Line', readonly=True) name = fields.Text(string='Description', required=True) display_type = fields.Selection([ ('line_section', "Section"), ('line_note', "Note")], default=False, help="Technical field for UX purpose.") quantity = fields.Float( string='Quantity', digits='Product Unit of Measure', readonly=True) price_subtotal = fields.Monetary( string='Amount', readonly=True) analytic_account_id = fields.Many2one( 'account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many( 'account.analytic.tag', string='Analytic Tags') currency_id = fields.Many2one('res.currency', readonly=True)