diff --git a/hotel/models/__init__.py b/hotel/models/__init__.py index c68f79c5b..46cfdf7ff 100644 --- a/hotel/models/__init__.py +++ b/hotel/models/__init__.py @@ -31,3 +31,4 @@ from . import hotel_service_line from . import hotel_board_service from . import hotel_board_service_room_type_line from . import hotel_board_service_line +from . import inherited_account_invoice_line diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py index 656aab0bb..23cf212a1 100644 --- a/hotel/models/hotel_folio.py +++ b/hotel/models/hotel_folio.py @@ -11,6 +11,8 @@ from dateutil.relativedelta import relativedelta from odoo.exceptions import except_orm, UserError, ValidationError from odoo.tools import ( misc, + float_is_zero, + float_compare, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT) from odoo import models, fields, api, _ @@ -44,9 +46,14 @@ class HotelFolio(models.Model): def _amount_all(self): pass + @api.model + def _get_default_team(self): + return self.env['crm.team']._get_default_team_id() + #Main Fields-------------------------------------------------------- name = fields.Char('Folio Number', readonly=True, index=True, default=lambda self: _('New')) + client_order_ref = fields.Char(string='Customer Reference', copy=False) partner_id = fields.Many2one('res.partner', track_visibility='onchange') @@ -87,6 +94,7 @@ class HotelFolio(models.Model): required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now) + confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the folio is confirmed.", copy=False) state = fields.Selection([ ('draft', 'Quotation'), ('sent', 'Quotation Sent'), @@ -111,6 +119,7 @@ class HotelFolio(models.Model): readonly=True) return_ids = fields.One2many('payment.return', 'folio_id', readonly=True) + payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term') #Amount Fields------------------------------------------------------ pending_amount = fields.Monetary(compute='compute_amount', @@ -150,12 +159,13 @@ class HotelFolio(models.Model): string='Invoice Status', 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)]}, - #~ help="Invoice address for current sales order.") + partner_invoice_id = fields.Many2one('res.partner', + string='Invoice Address', + readonly=True, required=True, + states={'draft': [('readonly', False)], + 'sent': [('readonly', False)]}, + help="Invoice address for current sales order.") + fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position') #WorkFlow Mail Fields----------------------------------------------- has_confirmed_reservations_to_send = fields.Boolean( @@ -179,6 +189,7 @@ class HotelFolio(models.Model): client_order_ref = fields.Char(string='Customer Reference', copy=False) note = fields.Text('Terms and conditions') sequence = fields.Integer(string='Sequence', default=10) + team_id = fields.Many2one('crm.team', 'Sales Channel', change_default=True, default=_get_default_team, oldname='section_id') @api.depends('room_lines.price_total','service_ids.price_total') def _amount_all(self): @@ -356,7 +367,7 @@ class HotelFolio(models.Model): if any(f not in vals for f in lfields): partner = self.env['res.partner'].browse(vals.get('partner_id')) addr = partner.address_get(['delivery', 'invoice']) - #~ vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) + vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) vals['pricelist_id'] = vals.setdefault( 'pricelist_id', partner.property_product_pricelist and partner.property_product_pricelist.id) @@ -373,11 +384,11 @@ class HotelFolio(models.Model): - user_id """ if not self.partner_id: - #~ self.update({ - #~ 'partner_invoice_id': False, - #~ 'payment_term_id': False, - #~ 'fiscal_position_id': False, - #~ }) + self.update({ + 'partner_invoice_id': False, + 'payment_term_id': False, + 'fiscal_position_id': False, + }) return addr = self.partner_id.address_get(['invoice']) pricelist = self.partner_id.property_product_pricelist and \ @@ -438,6 +449,102 @@ class HotelFolio(models.Model): 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 ''' @@ -483,7 +590,21 @@ class HotelFolio(models.Model): @api.multi def action_confirm(self): - _logger.info('action_confirm') + for folio in self.filtered(lambda folio: folio.partner_id not in folio.message_partner_ids): + folio.message_subscribe([folio.partner_id.id]) + self.write({ + 'state': 'confirm', + 'confirmation_date': fields.Datetime.now() + }) + #~ if self.env.context.get('send_email'): + #~ self.force_quotation_send() + + # create an analytic account if at least an expense product + #~ if any([expense_policy != 'no' for expense_policy in self.order_line.mapped('product_id.expense_policy')]): + #~ if not self.analytic_account_id: + #~ self._create_analytic_account() + + return True """ diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 2a43ded84..0ad40862e 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -8,6 +8,8 @@ from lxml import etree from odoo.exceptions import UserError, ValidationError from odoo.tools import ( misc, + float_is_zero, + float_compare, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT) from odoo import models, fields, api, _ @@ -75,6 +77,7 @@ class HotelReservation(models.Model): return folio.room_lines[0].departure_hour else: return default_departure_hour + @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): @@ -115,6 +118,7 @@ class HotelReservation(models.Model): ).days name = fields.Text('Reservation Description', required=True) + sequence = fields.Integer(string='Sequence', default=10) room_id = fields.Many2one('hotel.room', string='Room') @@ -230,8 +234,6 @@ class HotelReservation(models.Model): # order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True, auto_join=True) # product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') # product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True) - # product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0) - currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Currency', readonly=True, required=True) @@ -244,12 +246,13 @@ class HotelReservation(models.Model): tax_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) - # qty_to_invoice = fields.Float( - # 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')) + 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')) + invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_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', @@ -275,7 +278,7 @@ class HotelReservation(models.Model): # FIXME discount per night discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) - # analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') + analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') @api.model def create(self, vals): @@ -1124,3 +1127,91 @@ class HotelReservation(models.Model): @api.multi def send_cancel_mail(self): return self.folio_id.send_cancel_mail() + + """ + 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 + + @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.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 + """ + 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 + diff --git a/hotel/models/hotel_reservation_line.py b/hotel/models/hotel_reservation_line.py index bc87355af..c5120ff65 100644 --- a/hotel/models/hotel_reservation_line.py +++ b/hotel/models/hotel_reservation_line.py @@ -17,6 +17,8 @@ class HotelReservationLine(models.Model): discount = fields.Float( string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) + invoiced = fields.Boolean('Invoiced') + @api.constrains('date') def constrains_duplicated_date(self): diff --git a/hotel/models/inherited_account_invoice.py b/hotel/models/inherited_account_invoice.py index be8626210..e42f483f3 100644 --- a/hotel/models/inherited_account_invoice.py +++ b/hotel/models/inherited_account_invoice.py @@ -45,16 +45,17 @@ class AccountInvoice(models.Model): @api.multi def _compute_dif_customer_payment(self): for inv in self: - 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 + 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 @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 new file mode 100644 index 000000000..cc3ca1328 --- /dev/null +++ b/hotel/models/inherited_account_invoice_line.py @@ -0,0 +1,13 @@ +# Copyright 2017 Alexandre Díaz +# Copyright 2017 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import api, fields, models, _ + +class AccountInvoiceLine(models.Model): + _inherit = 'account.invoice.line' + + reservation_ids = fields.Many2many( + 'hotel.reservation', + 'reservation_line_invoice_rel', + 'invoice_line_id', 'reservation_id', + string='Reservations', readonly=True, copy=False) diff --git a/hotel/views/hotel_folio_views.xml b/hotel/views/hotel_folio_views.xml index 71d5cdc71..51a6df265 100644 --- a/hotel/views/hotel_folio_views.xml +++ b/hotel/views/hotel_folio_views.xml @@ -20,9 +20,9 @@ attrs="{'invisible': [('has_cancelled_reservations_to_send', '=', False)]}" class="oe_highlight"/> --> - + attrs="{'invisible': [('state', '!=', 'confirm')]}"/>