From b2c8305b2651245d085b3c0d05efe3e70ab17d6c Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Thu, 10 Jan 2019 15:21:10 +0100 Subject: [PATCH] [WIP] Invoice WorkFlow --- hotel/models/hotel_folio.py | 206 ++++------- hotel/models/hotel_reservation.py | 145 ++++---- hotel/models/hotel_service.py | 110 +++++- hotel/models/inherited_account_invoice.py | 17 +- .../models/inherited_account_invoice_line.py | 5 + hotel/views/hotel_folio_views.xml | 321 +----------------- hotel/views/hotel_reservation_views.xml | 159 +++++---- hotel/views/hotel_service_views.xml | 55 ++- .../views/inherited_account_invoice_views.xml | 12 +- hotel/wizard/folio_make_invoice_advance.py | 290 ++++++++++++---- .../folio_make_invoice_advance_views.xml | 73 ++-- 11 files changed, 666 insertions(+), 727 deletions(-) diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py index 23cf212a1..1f968b977 100644 --- a/hotel/models/hotel_folio.py +++ b/hotel/models/hotel_folio.py @@ -33,9 +33,57 @@ class HotelFolio(models.Model): # @api.depends('product_id.invoice_policy', 'order_id.state') def _compute_qty_delivered_updateable(self): pass - # @api.depends('state', 'order_line.invoice_status') + + @api.depends('state', 'room_lines.invoice_status', 'service_ids.invoice_status') def _get_invoiced(self): - pass + """ + Compute the invoice status of a Folio. Possible statuses: + - no: if the Folio is not in status 'sale' or 'done', we consider that there is nothing to + invoice. This is also the default value if the conditions of no other status is met. + - to invoice: if any Folio line is 'to invoice', the whole Folio is 'to invoice' + - invoiced: if all Folio lines are invoiced, the Folio is invoiced. + - upselling: if all Folio lines are invoiced or upselling, the status is upselling. + + The invoice_ids are obtained thanks to the invoice lines of the Folio lines, and we also search + for possible refunds created directly from existing invoices. This is necessary since such a + refund is not directly linked to the Folio. + """ + for folio in self: + invoice_ids = folio.room_lines.mapped('invoice_line_ids').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund']) + invoice_ids |= folio.service_ids.mapped('invoice_line_ids').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund']) + # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in + # 'account.invoice.refund') + # use like as origin may contains multiple references (e.g. 'SO01, SO02') + refunds = invoice_ids.search([('origin', 'like', folio.name), ('company_id', '=', folio.company_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund']) + invoice_ids |= refunds.filtered(lambda r: folio.id in r.folio_ids.ids) + # Search for refunds as well + refund_ids = self.env['account.invoice'].browse() + if invoice_ids: + for inv in invoice_ids: + refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)]) + + # Ignore the status of the deposit product + deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() + #~ line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id] + + #~ TODO: REVIEW INVOICE_STATUS + #~ if folio.state not in ('confirm', 'done'): + #~ invoice_status = 'no' + #~ elif any(invoice_status == 'to invoice' for invoice_status in line_invoice_status): + #~ invoice_status = 'to invoice' + #~ elif all(invoice_status == 'invoiced' for invoice_status in line_invoice_status): + #~ invoice_status = 'invoiced' + #~ elif all(invoice_status in ['invoiced', 'upselling'] for invoice_status in line_invoice_status): + #~ invoice_status = 'upselling' + #~ else: + #~ invoice_status = 'no' + + folio.update({ + 'invoice_count': len(set(invoice_ids.ids + refund_ids.ids)), + 'invoice_ids': invoice_ids.ids + refund_ids.ids, + #~ 'invoice_status': invoice_status + }) + # @api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced') def _compute_invoice_status(self): pass @@ -68,8 +116,8 @@ class HotelFolio(models.Model): help="Hotel services detail provide to " "customer and it will include in " "main Invoice.") - company_id = fields.Many2one('res.company', 'Company') - + company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('hotel.folio')) + analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a folio.", copy=False) currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Currency', readonly=True, required=True) @@ -148,8 +196,7 @@ class HotelFolio(models.Model): compute='_compute_checkin_partner_count') #Invoice Fields----------------------------------------------------- - hotel_invoice_id = fields.Many2one('account.invoice', 'Invoice') - num_invoices = fields.Integer(compute='_compute_num_invoices') + invoice_count = fields.Integer(compute='_get_invoiced') invoice_ids = fields.Many2many('account.invoice', string='Invoices', compute='_get_invoiced', readonly=True, copy=False) invoice_status = fields.Selection([('upselling', 'Upselling Opportunity'), @@ -160,10 +207,8 @@ class HotelFolio(models.Model): compute='_compute_invoice_status', store=True, readonly=True, default='no') partner_invoice_id = fields.Many2one('res.partner', - string='Invoice Address', - readonly=True, required=True, - states={'draft': [('readonly', False)], - 'sent': [('readonly', False)]}, + string='Invoice Address', required=True, + states={'done': [('readonly', True)]}, help="Invoice address for current sales order.") fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position') @@ -212,12 +257,6 @@ class HotelFolio(models.Model): for record in self: record.rooms_char = ', '.join(record.mapped('room_lines.room_id.name')) - @api.multi - def _compute_num_invoices(self): - pass - # for fol in self: - # fol.num_invoices = len(self.mapped('invoice_ids.id')) - # @api.depends('order_line.price_total', 'payment_ids', 'return_ids') @api.multi def compute_amount(self): @@ -380,8 +419,9 @@ class HotelFolio(models.Model): """ Update the following fields when the partner is changed: - Pricelist + - Payment terms - Invoice address - - user_id + - Delivery address """ if not self.partner_id: self.update({ @@ -390,13 +430,18 @@ class HotelFolio(models.Model): 'fiscal_position_id': False, }) return + addr = self.partner_id.address_get(['invoice']) pricelist = self.partner_id.property_product_pricelist and \ self.partner_id.property_product_pricelist.id or \ self.env['ir.default'].sudo().get('res.config.settings', 'default_pricelist_id') - values = {'user_id': self.partner_id.user_id.id or self.env.uid, - 'pricelist_id': pricelist - } + values = { + 'pricelist_id': pricelist, + 'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False, + 'partner_invoice_id': addr['invoice'], + 'user_id': self.partner_id.user_id.id or self.env.uid + } + if self.env['ir.config_parameter'].sudo().get_param('sale.use_sale_note') and \ self.env.user.company_id.sale_note: values['note'] = self.with_context( @@ -424,127 +469,6 @@ class HotelFolio(models.Model): else: return 'normal' - @api.multi - def action_invoice_create(self, grouped=False, states=None): - ''' - @param self: object pointer - ''' - pass - # if states is None: - # states = ['confirmed', 'done'] - # order_ids = [folio.order_id.id for folio in self] - # sale_obj = self.env['sale.order'].browse(order_ids) - # invoice_id = (sale_obj.action_invoice_create(grouped=False, - # states=['confirmed', - # 'done'])) - # for line in self: - # values = {'invoiced': True, - # 'state': 'progress' if grouped else 'progress', - # 'hotel_invoice_id': invoice_id - # } - # line.write(values) - # return invoice_id - - @api.multi - def advance_invoice(self): - pass - - @api.multi - def _prepare_invoice(self): - """ - Prepare the dict of values to create the new invoice for a sales order. This method may be - overridden to implement custom invoice generation (making sure to call super() to establish - a clean extension chain). - """ - self.ensure_one() - journal_id = self.env['account.invoice'].default_get(['journal_id'])['journal_id'] - if not journal_id: - raise UserError(_('Please define an accounting sales journal for this company.')) - import wdb; wdb.set_trace() - invoice_vals = { - 'name': self.client_order_ref or '', - 'origin': self.name, - 'type': 'out_invoice', - 'account_id': self.partner_invoice_id.property_account_receivable_id.id, - 'partner_id': self.partner_invoice_id.id, - 'partner_shipping_id': self.partner_id.id, - 'journal_id': journal_id, - 'currency_id': self.pricelist_id.currency_id.id, - 'comment': self.note, - 'payment_term_id': self.payment_term_id.id, - 'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id, - 'company_id': self.company_id.id, - 'user_id': self.user_id and self.user_id.id, - 'team_id': self.team_id.id - } - return invoice_vals - - @api.multi - def action_invoice_create(self, grouped=False): - """ - Create the invoice associated to the Folio. - :param grouped: if True, invoices are grouped by Folio id. If False, invoices are grouped by - (partner_invoice_id, currency) - :returns: list of created invoices - """ - inv_obj = self.env['account.invoice'] - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - invoices = {} - references = {} - invoices_origin = {} - invoices_name = {} - - for folio in self: - group_key = folio.id if grouped else (folio.partner_invoice_id.id, folio.currency_id.id) - for line in folio.room_lines.sorted(key=lambda l: l.qty_to_invoice < 0): - if float_is_zero(line.qty_to_invoice, precision_digits=precision): - continue - if group_key not in invoices: - inv_data = folio._prepare_invoice() - invoice = inv_obj.create(inv_data) - references[invoice] = folio - invoices[group_key] = invoice - invoices_origin[group_key] = [invoice.origin] - invoices_name[group_key] = [invoice.name] - elif group_key in invoices: - if folio.name not in invoices_origin[group_key]: - invoices_origin[group_key].append(folio.name) - if folio.client_order_ref and folio.client_order_ref not in invoices_name[group_key]: - invoices_name[group_key].append(folio.client_order_ref) - - if line.qty_to_invoice > 0: - line.invoice_line_create(invoices[group_key].id, line.nights) - - if references.get(invoices.get(group_key)): - if folio not in references[invoices[group_key]]: - references[invoices[group_key]] |= folio - - for group_key in invoices: - invoices[group_key].write({'name': ', '.join(invoices_name[group_key]), - 'origin': ', '.join(invoices_origin[group_key])}) - - if not invoices: - raise UserError(_('There is no invoiceable line.')) - - for invoice in invoices.values(): - if not invoice.invoice_line_ids: - raise UserError(_('There is no invoiceable line.')) - # If invoice is negative, do a refund invoice instead - if invoice.amount_untaxed < 0: - invoice.type = 'out_refund' - for line in invoice.invoice_line_ids: - line.quantity = -line.quantity - # Use additional field helper function (for account extensions) - for line in invoice.invoice_line_ids: - line._set_additional_fields(invoice) - # Necessary to force computation of taxes. In account_invoice, they are triggered - # by onchanges, which are not triggered when doing a create. - invoice.compute_taxes() - invoice.message_post_with_view('mail.message_origin_link', - values={'self': invoice, 'origin': references[invoice]}, - subtype_id=self.env.ref('mail.mt_note').id) - return [inv.id for inv in invoices.values()] - ''' WORKFLOW STATE ''' diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 0ad40862e..2dd7a37c9 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -77,6 +77,36 @@ class HotelReservation(models.Model): return folio.room_lines[0].departure_hour else: return default_departure_hour + + @api.depends('state', 'qty_to_invoice', 'qty_invoiced') + def _compute_invoice_status(self): + """ + Compute the invoice status of a Reservation. Possible statuses: + - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to + invoice. This is also hte default value if the conditions of no other status is met. + - to invoice: we refer to the quantity to invoice of the line. Refer to method + `_get_to_invoice_qty()` for more information on how this quantity is calculated. + - upselling: this is possible only for a product invoiced on ordered quantities for which + we delivered more than expected. The could arise if, for example, a project took more + time than expected but we decided not to invoice the extra cost to the client. This + occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity + is removed from the list. + - invoiced: the quantity invoiced is larger or equal to the quantity ordered. + """ + pass + #~ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + #~ for line in self: + #~ if line.state not in ('confirm', 'done'): + #~ line.invoice_status = 'no' + #~ elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): + #~ line.invoice_status = 'to invoice' + #~ elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\ + #~ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1: + #~ line.invoice_status = 'upselling' + #~ elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0: + #~ line.invoice_status = 'invoiced' + #~ else: + #~ line.invoice_status = 'no' @api.model @@ -170,7 +200,7 @@ class HotelReservation(models.Model): partner_id = fields.Many2one(related='folio_id.partner_id') closure_reason_id = fields.Many2one(related='folio_id.closure_reason_id') - company_id = fields.Many2one('res.company', 'Company') + company_id = fields.Many2one(related='folio_id.company_id', string='Company', store=True, readonly=True) reservation_line_ids = fields.One2many('hotel.reservation.line', 'reservation_id', readonly=True, required=True, @@ -237,13 +267,13 @@ class HotelReservation(models.Model): currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Currency', readonly=True, required=True) - # invoice_status = fields.Selection([ - # ('upselling', 'Upselling Opportunity'), - # ('invoiced', 'Fully Invoiced'), - # ('to invoice', 'To Invoice'), - # ('no', 'Nothing to Invoice') - # ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no') - tax_id = fields.Many2many('account.tax', + invoice_status = fields.Selection([ + ('upselling', 'Upselling Opportunity'), + ('invoiced', 'Fully Invoiced'), + ('to invoice', 'To Invoice'), + ('no', 'Nothing to Invoice') + ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no') + tax_ids = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) qty_to_invoice = fields.Float( @@ -252,7 +282,7 @@ class HotelReservation(models.Model): qty_invoiced = fields.Float( compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True, digits=dp.get_precision('Product Unit of Measure')) - invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False) + invoice_line_ids = fields.Many2many('account.invoice.line', 'reservation_line_invoice_rel', 'reservation_id', 'invoice_line_id', string='Invoice Lines', copy=False) # qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0) # qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True) price_subtotal = fields.Monetary(string='Subtotal', @@ -765,7 +795,7 @@ class HotelReservation(models.Model): return True return False - @api.depends('reservation_line_ids', 'reservation_line_ids.discount', 'tax_id') + @api.depends('reservation_line_ids', 'reservation_line_ids.discount', 'tax_ids') def _compute_amount_reservation(self): """ Compute the amounts of the reservation. @@ -775,7 +805,7 @@ class HotelReservation(models.Model): if amount_room > 0: product = record.room_type_id.product_id price = amount_room * (1 - (record.discount or 0.0) * 0.01) - taxes = record.tax_id.compute_all(price, record.currency_id, 1, product=product) + taxes = record.tax_ids.compute_all(price, record.currency_id, 1, product=product) record.update({ 'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])), 'price_total': taxes['total_included'], @@ -809,7 +839,7 @@ class HotelReservation(models.Model): pricelist=pricelist_id, uom=product.uom_id.id) line_price = self.env['account.tax']._fix_tax_included_price_company( - product.price, product.taxes_id, self.tax_id, self.company_id) + product.price, product.taxes_id, self.tax_ids, self.company_id) if old_line: cmds.append((1, old_line.id, { 'price': line_price @@ -1131,22 +1161,24 @@ class HotelReservation(models.Model): """ INVOICING PROCESS """ + @api.depends('qty_invoiced', 'nights', 'folio_id.state') def _get_to_invoice_qty(self): """ Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is calculated from the ordered quantity. Otherwise, the quantity delivered is used. """ - for line in self: - if line.folio_id.state in ['confirm', 'done']: - if line.room_type_id.product_id.invoice_policy == 'order': - line.qty_to_invoice = line.nights - line.qty_invoiced - else: - line.qty_to_invoice = line.qty_delivered - line.qty_invoiced - else: - line.qty_to_invoice = 0 + pass + #~ for line in self: + #~ if line.folio_id.state in ['confirm', 'done']: + #~ if line.room_type_id.product_id.invoice_policy == 'order': + #~ line.qty_to_invoice = line.nights - line.qty_invoiced + #~ else: + #~ line.qty_to_invoice = line.qty_delivered - line.qty_invoiced + #~ else: + #~ line.qty_to_invoice = 0 - @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity') + @api.depends('invoice_line_ids.invoice_id.state', 'invoice_line_ids.quantity') def _get_invoice_qty(self): """ Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note @@ -1154,64 +1186,13 @@ class HotelReservation(models.Model): a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing it automatically, which may not be wanted at all. That's why the refund has to be created from the SO """ - for line in self: - qty_invoiced = 0.0 - for invoice_line in line.invoice_lines: - if invoice_line.invoice_id.state != 'cancel': - if invoice_line.invoice_id.type == 'out_invoice': - qty_invoiced += invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom) - elif invoice_line.invoice_id.type == 'out_refund': - qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom) - line.qty_invoiced = qty_invoiced - - @api.multi - def _prepare_invoice_line(self, qty): - """ - Prepare the dict of values to create the new invoice line for a reservation. - - :param qty: float quantity to invoice - """ - self.ensure_one() - res = {} - product = self.env['product.product'].browse(self.room_type_id.product_id.id) - account = product.property_account_income_id or product.categ_id.property_account_income_categ_id - if not account: - raise UserError(_('Please define income account for this product: "%s" (id:%d) - or for its category: "%s".') % - (product.name, product.id, product.categ_id.name)) - - fpos = self.folio_id.fiscal_position_id or self.folio_id.partner_id.property_account_position_id - if fpos: - account = fpos.map_account(account) - - res = { - 'name': self.name, - 'sequence': self.sequence, - 'origin': self.folio_id.name, - 'account_id': account.id, - 'price_unit': self.price_unit, - 'quantity': qty, - 'discount': self.discount, - 'uom_id': self.product_uom.id, - 'product_id': product.id or False, - 'invoice_line_tax_ids': [(6, 0, self.tax_id.ids)], - 'account_analytic_id': self.folio_id.analytic_account_id.id, - 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)], - } - return res - - @api.multi - def invoice_line_create(self, invoice_id, qty): - """ Create an invoice line. The quantity to invoice can be positive (invoice) or negative (refund). - :param invoice_id: integer - :param qty: float quantity to invoice - :returns recordset of account.invoice.line created - """ - invoice_lines = self.env['account.invoice.line'] - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - for line in self: - if not float_is_zero(qty, precision_digits=precision): - vals = line._prepare_invoice_line(qty=qty) - vals.update({'invoice_id': invoice_id, 'reservation_ids': [(6, 0, [line.id])]}) - invoice_lines |= self.env['account.invoice.line'].create(vals) - return invoice_lines - + pass + #~ for line in self: + #~ qty_invoiced = 0.0 + #~ for invoice_line in line.invoice_line_ids: + #~ if invoice_line.invoice_id.state != 'cancel': + #~ if invoice_line.invoice_id.type == 'out_invoice': + #~ qty_invoiced += invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom) + #~ elif invoice_line.invoice_id.type == 'out_refund': + #~ qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom) + #~ line.qty_invoiced = qty_invoiced diff --git a/hotel/models/hotel_service.py b/hotel/models/hotel_service.py index eac017044..e7a21aade 100644 --- a/hotel/models/hotel_service.py +++ b/hotel/models/hotel_service.py @@ -44,7 +44,73 @@ class HotelService(models.Model): (ids)], limit=1) return False - name = fields.Char('Service description') + @api.depends('qty_invoiced', 'product_qty', 'folio_id.state') + def _get_to_invoice_qty(self): + """ + Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is + calculated from the ordered quantity. Otherwise, the quantity delivered is used. + """ + pass + #~ for line in self: + #~ if line.folio_id.state in ['confirm', 'done']: + #~ if line.room_type_id.product_id.invoice_policy == 'order': + #~ line.qty_to_invoice = line.nights - line.qty_invoiced + #~ else: + #~ line.qty_to_invoice = line.qty_delivered - line.qty_invoiced + #~ else: + #~ line.qty_to_invoice = 0 + + @api.depends('invoice_line_ids.invoice_id.state', 'invoice_line_ids.quantity') + def _get_invoice_qty(self): + """ + Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note + that this is the case only if the refund is generated from the SO and that is intentional: if + a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing + it automatically, which may not be wanted at all. That's why the refund has to be created from the SO + """ + pass + #~ for line in self: + #~ qty_invoiced = 0.0 + #~ for invoice_line in line.invoice_line_ids: + #~ if invoice_line.invoice_id.state != 'cancel': + #~ if invoice_line.invoice_id.type == 'out_invoice': + #~ qty_invoiced += invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom) + #~ elif invoice_line.invoice_id.type == 'out_refund': + #~ qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom) + #~ line.qty_invoiced = qty_invoiced + + @api.depends('product_qty', 'qty_to_invoice', 'qty_invoiced') + def _compute_invoice_status(self): + """ + Compute the invoice status of a SO line. Possible statuses: + - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to + invoice. This is also hte default value if the conditions of no other status is met. + - to invoice: we refer to the quantity to invoice of the line. Refer to method + `_get_to_invoice_qty()` for more information on how this quantity is calculated. + - upselling: this is possible only for a product invoiced on ordered quantities for which + we delivered more than expected. The could arise if, for example, a project took more + time than expected but we decided not to invoice the extra cost to the client. This + occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity + is removed from the list. + - invoiced: the quantity invoiced is larger or equal to the quantity ordered. + """ + pass + #~ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + #~ for line in self: + #~ if line.state not in ('sale', 'done'): + #~ line.invoice_status = 'no' + #~ elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): + #~ line.invoice_status = 'to invoice' + #~ elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\ + #~ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1: + #~ line.invoice_status = 'upselling' + #~ elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0: + #~ line.invoice_status = 'invoiced' + #~ else: + #~ line.invoice_status = 'no' + + name = fields.Char('Service description', required=True) + sequence = fields.Integer(string='Sequence', default=10) product_id = fields.Many2one('product.product', 'Service', required=True) folio_id = fields.Many2one('hotel.folio', 'Folio', ondelete='cascade') ser_room_line = fields.Many2one('hotel.reservation', 'Room', @@ -57,6 +123,12 @@ class HotelService(models.Model): # Non-stored related field to allow portal user to see the image of the product he has ordered product_image = fields.Binary('Product Image', related="product_id.image", store=False) company_id = fields.Many2one(related='folio_id.company_id', string='Company', store=True, readonly=True) + invoice_status = fields.Selection([ + ('upselling', 'Upselling Opportunity'), + ('invoiced', 'Fully Invoiced'), + ('to invoice', 'To Invoice'), + ('no', 'Nothing to Invoice') + ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no') channel_type = fields.Selection([ ('door', 'Door'), ('mail', 'Mail'), @@ -67,6 +139,14 @@ class HotelService(models.Model): tax_ids = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) currency_id = fields.Many2one(related='folio_id.currency_id', store=True, string='Currency', readonly=True) + invoice_line_ids = fields.Many2many('account.invoice.line', 'service_line_invoice_rel', 'service_id', 'invoice_line_id', string='Invoice Lines', copy=False) + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') + qty_to_invoice = fields.Float( + compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True, + digits=dp.get_precision('Product Unit of Measure')) + qty_invoiced = fields.Float( + compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True, + digits=dp.get_precision('Product Unit of Measure')) price_subtotal = fields.Monetary(string='Subtotal', readonly=True, store=True, @@ -130,7 +210,7 @@ class HotelService(models.Model): if values.get('product_id'): line = self.new(values) if any(f not in values for f in onchange_fields): - line.onchange_product_calc_qty() + line.onchange_product_id() for field in onchange_fields: if field not in values: res[field] = line._fields[field].convert_to_write(line[field], line) @@ -171,7 +251,7 @@ class HotelService(models.Model): return max(base_price, final_price) @api.onchange('product_id') - def onchange_product_calc_qty(self): + def onchange_product_id(self): """ Compute the default quantity according to the configuration of the selected product, in per_day product configuration, @@ -195,6 +275,30 @@ class HotelService(models.Model): for day in record.service_line_ids: day.no_free_resources() """ + Description and warnings + """ + product = self.product_id.with_context( + lang=self.folio_id.partner_id.lang, + partner=self.folio_id.partner_id.id + ) + title = False + message = False + warning = {} + if product.sale_line_warn != 'no-message': + title = _("Warning for %s") % product.name + message = product.sale_line_warn_msg + warning['title'] = title + warning['message'] = message + result = {'warning': warning} + if product.sale_line_warn == 'block': + self.product_id = False + return result + + name = product.name_get()[0][1] + if product.description_sale: + name += '\n' + product.description_sale + vals['name'] = name + """ Compute tax and price unit """ self._compute_tax_ids() diff --git a/hotel/models/inherited_account_invoice.py b/hotel/models/inherited_account_invoice.py index e42f483f3..1e67465fe 100644 --- a/hotel/models/inherited_account_invoice.py +++ b/hotel/models/inherited_account_invoice.py @@ -33,7 +33,6 @@ class AccountInvoice(models.Model): 'domain': [('id', 'in', payment_ids)], } - dif_customer_payment = fields.Boolean(compute='_compute_dif_customer_payment') from_folio = fields.Boolean(compute='_compute_dif_customer_payment') sale_ids = fields.Many2many( 'sale.order', 'sale_order_invoice_rel', 'invoice_id', @@ -45,17 +44,11 @@ class AccountInvoice(models.Model): @api.multi def _compute_dif_customer_payment(self): for inv in self: - return False - #~ sales = inv.mapped('invoice_line_ids.sale_line_ids.order_id') - #~ folios = self.env['hotel.folio'].search([('order_id.id','in',sales.ids)]) - #~ if folios: - #~ inv.from_folio = True - #~ inv.folio_ids = [(6, 0, folios.ids)] - #~ payments_obj = self.env['account.payment'] - #~ payments = payments_obj.search([('folio_id','in',folios.ids)]) - #~ for pay in payments: - #~ if pay.partner_id != inv.partner_id: - #~ inv.dif_customer_payment = True + folios = inv.mapped('invoice_line_ids.reservation_ids.folio_id') + folios |= inv.mapped('invoice_line_ids.service_ids.folio_id') + if folios: + inv.from_folio = True + inv.folio_ids = [(6, 0, folios.ids)] @api.multi def action_invoice_open(self): diff --git a/hotel/models/inherited_account_invoice_line.py b/hotel/models/inherited_account_invoice_line.py index cc3ca1328..1c251cc37 100644 --- a/hotel/models/inherited_account_invoice_line.py +++ b/hotel/models/inherited_account_invoice_line.py @@ -11,3 +11,8 @@ class AccountInvoiceLine(models.Model): 'reservation_line_invoice_rel', 'invoice_line_id', 'reservation_id', string='Reservations', readonly=True, copy=False) + service_ids = fields.Many2many( + 'hotel.service', + 'service_line_invoice_rel', + 'invoice_line_id', 'service_id', + string='Services', readonly=True, copy=False) diff --git a/hotel/views/hotel_folio_views.xml b/hotel/views/hotel_folio_views.xml index 51a6df265..742454e19 100644 --- a/hotel/views/hotel_folio_views.xml +++ b/hotel/views/hotel_folio_views.xml @@ -108,17 +108,17 @@ --> - + @@ -152,315 +152,26 @@ - - - - - -