# Part of Odoo. See LICENSE file for full copyright and licensing details. import time from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo import api, fields, models, _ import odoo.addons.decimal_precision as dp from odoo.exceptions import UserError, ValidationError from datetime import timedelta class FolioAdvancePaymentInv(models.TransientModel): _name = "folio.advance.payment.inv" _description = "Folios Advance Payment Invoice" @api.model def _get_advance_payment_method(self): return 'all' @api.model def _default_product_id(self): product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') return self.env['product.product'].browse(int(product_id)) @api.model def _get_default_folio(self): if self._context.get('default_reservation_id'): folio_ids = self._context.get('default_folio_id', []) else: folio_ids = self._context.get('active_ids', []) folios = self.env['hotel.folio'].browse(folio_ids) return folios @api.model def _get_default_reservation(self): if self._context.get('default_reservation_id'): reservations = self.env['hotel.reservation'].browse(self._context.get('active_ids', [])) else: folios = self._get_default_folio() reservations = self.env['hotel.reservation'] for folio in folios: reservations |= folio.room_lines return reservations @api.model def _get_default_partner_invoice(self): folios = self._get_default_folio() if folios[0].tour_operator_id: return folios[0].tour_operator_id return folios[0].partner_invoice_id @api.model def _default_deposit_account_id(self): return self._default_product_id().property_account_income_id @api.model def _default_deposit_taxes_id(self): return self._default_product_id().taxes_id advance_payment_method = fields.Selection([ ('all', 'Invoiceable lines (deduct down payments)'), ('percentage', 'Down payment (percentage)'), ('fixed', 'Down payment (fixed amount)') ], string='What do you want to invoice?', default=_get_advance_payment_method, required=True) auto_invoice = fields.Boolean('Auto Payment Invoice', default=True, help='Automatic validation and link payment to invoice') count = fields.Integer(compute='_count', store=True, string='# of Orders') folio_ids = fields.Many2many("hotel.folio", string="Folios", help="Folios grouped", default=_get_default_folio) reservation_ids = fields.Many2many("hotel.reservation", string="Rooms", help="Folios grouped", default=_get_default_reservation) group_folios = fields.Boolean('Group Folios') partner_invoice_id = fields.Many2one('res.partner', string='Invoice Address', required=True, default=_get_default_partner_invoice, help="Invoice address for current Invoice.") line_ids = fields.One2many('line.advance.inv', 'advance_inv_id', string="Invoice Lines") #Advance Payment product_id = fields.Many2one('product.product', string="Product", domain=[('type', '=', 'service')], default=_default_product_id) amount = fields.Float('Down Payment Amount', digits=dp.get_precision('Account'), help="The amount to be invoiced in advance, taxes excluded.") deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)], help="Account used for deposits", default=_default_deposit_account_id) deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id) @api.depends('folio_ids') def _count(self): for record in self: record.update({'count': len(self.folio_ids)}) @api.onchange('advance_payment_method') def onchange_advance_payment_method(self): if self.advance_payment_method == 'percentage': return {'value': {'amount': 0}} return {} @api.multi def _create_invoice(self, folio, service, amount): inv_obj = self.env['account.invoice'] ir_property_obj = self.env['ir.property'] account_id = False if self.product_id.id: account_id = self.product_id.property_account_income_id.id \ or self.product_id.categ_id.property_account_income_categ_id.id if not account_id: inc_acc = ir_property_obj.get('property_account_income_categ_id', 'product.category') account_id = folio.fiscal_position_id.map_account(inc_acc).id if inc_acc else False if not account_id: raise UserError( _('There is no income account defined for this product: "%s". You may have to install a chart of account from Accounting app, settings menu.') % (self.product_id.name,)) if self.amount <= 0.00: raise UserError(_('The value of the down payment amount must be positive.')) context = {'lang': folio.partner_id.lang} if self.advance_payment_method == 'percentage': amount = folio.amount_untaxed * self.amount / 100 name = _("Down payment of %s%%") % (self.amount,) else: amount = self.amount name = _('Down Payment') del context taxes = self.product_id.taxes_id.filtered( lambda r: not folio.company_id or r.company_id == folio.company_id) if folio.fiscal_position_id and taxes: tax_ids = folio.fiscal_position_id.map_tax(taxes).ids else: tax_ids = taxes.ids invoice = inv_obj.create({ 'name': folio.client_order_ref or folio.name, 'origin': folio.name, 'type': 'out_invoice', 'reference': False, 'folio_ids': [(6, 0, [folio.id])], 'account_id': folio.partner_id.property_account_receivable_id.id, 'partner_id': folio.partner_invoice_id.id, 'invoice_line_ids': [(0, 0, { 'name': name, 'origin': folio.name, 'account_id': account_id, 'price_unit': amount, 'quantity': 1.0, 'discount': 0.0, 'uom_id': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'service_ids': [(6, 0, [service.id])], 'invoice_line_tax_ids': [(6, 0, tax_ids)], 'account_analytic_id': folio.analytic_account_id.id or False, })], 'currency_id': folio.pricelist_id.currency_id.id, 'payment_term_id': folio.payment_term_id.id, 'fiscal_position_id': folio.fiscal_position_id.id \ or folio.partner_id.property_account_position_id.id, 'team_id': folio.team_id.id, 'user_id': folio.user_id.id, 'comment': folio.note, }) invoice.compute_taxes() invoice.message_post_with_view( 'mail.message_origin_link', values={'self': invoice, 'origin': folio}, subtype_id=self.env.ref('mail.mt_note').id) return invoice @api.model def _validate_invoices(self, invoice): if self.auto_invoice: invoice.action_invoice_open() payment_ids = self.folio_ids.mapped('payment_ids.id') domain = [ ('account_id', '=', invoice.account_id.id), ('payment_id', 'in', payment_ids), ('reconciled', '=', False), '|', ('amount_residual', '!=', 0.0), ('amount_residual_currency', '!=', 0.0) ] if invoice.type in ('out_invoice', 'in_refund'): domain.extend([('credit', '>', 0), ('debit', '=', 0)]) type_payment = _('Outstanding credits') else: domain.extend([('credit', '=', 0), ('debit', '>', 0)]) type_payment = _('Outstanding debits') info = {'title': '', 'outstanding': True, 'content': [], 'invoice_id': invoice.id} lines = self.env['account.move.line'].search(domain) currency_id = invoice.currency_id for line in lines: invoice.assign_outstanding_credit(line.id) return True @api.multi def create_invoices(self): inv_obj = self.env['account.invoice'] precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') folios = self.folio_ids if not self.partner_invoice_id or not self.partner_invoice_id.vat: vat_error = _("We need the VAT of the customer") raise ValidationError(vat_error) if self.advance_payment_method == 'all': inv_data = self._prepare_invoice() invoice = inv_obj.create(inv_data) for line in self.line_ids: line.invoice_line_create(invoice.id, line.qty) else: # Create deposit product if necessary if not self.product_id: vals = self._prepare_deposit_product() self.product_id = self.env['product.product'].sudo().create(vals) self.env['ir.config_parameter'].sudo().set_param( 'sale.default_deposit_product_id', self.product_id.id) service_obj = self.env['hotel.service'] for folio in folios: if self.advance_payment_method == 'percentage': amount = folio.amount_untaxed * folio.amount_total / 100 else: amount = self.amount if self.product_id.invoice_policy != 'order': raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) if self.product_id.type != 'service': raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) taxes = self.product_id.taxes_id.filtered( lambda r: not folio.company_id or r.company_id == folio.company_id) if folio.fiscal_position_id and taxes: tax_ids = folio.fiscal_position_id.map_tax(taxes).ids else: tax_ids = taxes.ids context = {'lang': folio.partner_id.lang} service_line = service_obj.create({ 'name': _('Advance: %s') % (time.strftime('%m %Y'),), 'price_unit': amount, 'product_qty': 0.0, 'folio_id': folio.id, 'discount': 0.0, 'product_uom': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'tax_id': [(6, 0, tax_ids)], }) del context invoice = self._create_invoice(folio, service_line, amount) invoice.compute_taxes() 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_total < 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() self._validate_invoices(invoice) invoice.message_post_with_view('mail.message_origin_link', values={'self': invoice, 'origin': folios}, subtype_id=self.env.ref('mail.mt_note').id) if self._context.get('open_invoices', False): return folios.open_invoices_folio() return {'type': 'ir.actions.act_window_close'} def _prepare_deposit_product(self): return { 'name': 'Down payment', 'type': 'service', 'invoice_policy': 'order', 'property_account_income_id': self.deposit_account_id.id, 'taxes_id': [(6, 0, self.deposit_taxes_id.ids)], } @api.onchange('reservation_ids') def prepare_invoice_lines(self): vals = [(5,0,0)] folios = self.folio_ids invoice_lines = {} for folio in folios: for service in folio.service_ids.filtered( lambda x: x.is_board_service == False and \ x.qty_to_invoice != 0 and \ (x.ser_room_line.id in self.reservation_ids.ids or \ not x.ser_room_line.id)): invoice_lines[service.id] = { 'description': service.name, 'product_id': service.product_id.id, 'qty': service.qty_to_invoice, 'discount': service.discount, 'price_unit': service.price_unit, 'service_id': service.id, } for reservation in folio.room_lines.filtered( lambda x: x.id in self.reservation_ids.ids and x.invoice_status == 'to invoice'): board_service = reservation.board_service_room_id for day in reservation.reservation_line_ids.filtered( lambda x: not x.invoice_line_ids).sorted('date'): extra_price = 0 if board_service: services = reservation.service_ids.filtered( lambda x: x.is_board_service == True) for service in services: service_date = day.date if service.product_id.consumed_on == 'after': service_date = (fields.Date.from_string(day.date) + \ timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) extra_price += service.price_unit * \ service.service_line_ids.filtered( lambda x: x.date == service_date).day_qty #group_key: if group by reservation, We no need group by room_type group_key = (reservation.id, reservation.room_type_id.id, day.price + extra_price, day.discount, day.cancel_discount) if day.cancel_discount == 100: continue discount_factor = 1.0 for discount in [day.discount, day.cancel_discount]: discount_factor = ( discount_factor * ((100.0 - discount) / 100.0)) final_discount = 100.0 - (discount_factor * 100.0) description = folio.name + ' ' + reservation.room_type_id.name + ' (' + \ reservation.board_service_room_id.hotel_board_service_id.name + ')' \ if board_service else folio.name + ' ' + reservation.room_type_id.name if group_key not in invoice_lines: invoice_lines[group_key] = { 'description': description, 'reservation_id': reservation.id, 'room_type_id': reservation.room_type_id, 'product_id': self.env['product.product'].browse( reservation.room_type_id.product_id.id), 'discount': final_discount, 'price_unit': day.price + extra_price, 'reservation_line_ids': [(4, day.id)] } else: invoice_lines[group_key][('reservation_line_ids')].append((4,day.id)) for group_key in invoice_lines: vals.append((0, False, invoice_lines[group_key])) self.line_ids = vals self.line_ids.onchange_reservation_line_ids() @api.onchange('folio_ids') def onchange_folio_ids(self): vals = [] folios = self.folio_ids invoice_lines = {} reservations = self.env['hotel.reservation'] services = self.env['hotel.service'] old_folio_ids = self.reservation_ids.mapped('folio_id.id') for folio in folios.filtered(lambda r: r.id not in old_folio_ids): folio_reservations = folio.room_lines if folio_reservations: reservations |= folio_reservations self.reservation_ids |= reservations self.prepare_invoice_lines() @api.model def _prepare_invoice(self): """ Prepare the dict of values to create the new invoice for a folio. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). """ 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.')) origin = ' '.join(self.folio_ids.mapped('name')) pricelist = self.folio_ids[0].pricelist_id currency = self.folio_ids[0].currency_id payment_term = self.folio_ids[0].payment_term_id fiscal_position = self.folio_ids[0].fiscal_position_id company = self.folio_ids[0].company_id user = self.folio_ids[0].user_id team = self.folio_ids[0].team_id for folio in self.folio_ids: if folio.pricelist_id != pricelist: raise UserError(_('All Folios must hace the same pricelist')) invoice_vals = { 'name': self.folio_ids[0].client_order_ref or '', 'origin': origin, 'type': 'out_invoice', 'account_id': self.partner_invoice_id.property_account_receivable_id.id, 'partner_id': self.partner_invoice_id.id, 'journal_id': journal_id, 'currency_id': currency.id, 'payment_term_id': payment_term.id, 'fiscal_position_id': fiscal_position.id or self.partner_invoice_id.property_account_position_id.id, 'company_id': company.id, 'user_id': user and user.id, 'team_id': team.id, 'comment': self.folio_ids[0].note } return invoice_vals class LineAdvancePaymentInv(models.TransientModel): _name = "line.advance.inv" _description = "Lines Advance Invoice" room_type_id = fields.Many2one('hotel.room.type') product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')]) qty = fields.Integer('Quantity') price_unit = fields.Float('Price Unit') price_total = fields.Float('Price Total', compute='_compute_price_total') price_tax = fields.Float('Price Tax', compute='_compute_price_total') price_subtotal = fields.Float('Price Subtotal', compute='_compute_price_total', store=True) advance_inv_id = fields.Many2one('folio.advance.payment.inv') price_room = fields.Float(compute='_compute_price_room') discount = fields.Float( string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) to_invoice = fields.Boolean('To Invoice') description = fields.Text('Description') description_dates = fields.Text('Range') reservation_id = fields.Many2one('hotel.reservation') service_id = fields.Many2one('hotel.service') folio_id = fields.Many2one('hotel.folio', compute='_compute_folio_id') reservation_line_ids = fields.Many2many( 'hotel.reservation.line', string='Reservation Lines') @api.depends('qty', 'price_unit', 'discount') def _compute_price_total(self): for record in self: origin = record.reservation_id if record.reservation_id.id else record.service_id amount_line = record.price_unit * record.qty if amount_line != 0: product = record.product_id price = amount_line * (1 - (record.discount or 0.0) * 0.01) taxes = origin.tax_ids.compute_all(price, origin.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'], 'price_subtotal': taxes['total_excluded'], }) def _compute_price_room(self): for record in self: if record.reservation_id: record.price_room = record.reservation_line_ids[0].price def _compute_folio_id(self): for record in self: origin = record.reservation_id if record.reservation_id.id else record.service_id record.folio_id = origin.folio_id @api.onchange('reservation_line_ids') def onchange_reservation_line_ids(self): for record in self: if record.reservation_id: if not record.reservation_line_ids: raise UserError(_('If you want drop the line, use the trash icon')) record.qty = len(record.reservation_line_ids) record.description_dates = record.reservation_line_ids[0].date + ' - ' + \ ((fields.Date.from_string(record.reservation_line_ids[-1].date)) + \ timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) @api.multi def invoice_line_create(self, invoice_id, qty): """ Create an invoice line. :param invoice_id: integer :param qty: float quantity to invoice :returns recordset of account.invoice.line created """ self.ensure_one() invoice_lines = self.env['account.invoice.line'] precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') origin = self.reservation_id if self.reservation_id.id else self.service_id product = self.product_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) vals = { 'sequence': origin.sequence, 'origin': origin.name, 'account_id': account.id, 'price_unit': self.price_unit, 'quantity': self.qty, 'discount': self.discount, 'uom_id': product.uom_id.id, 'product_id': product.id or False, 'invoice_line_tax_ids': [(6, 0, origin.tax_ids.ids)], 'account_analytic_id': self.folio_id.analytic_account_id.id, 'analytic_tag_ids': [(6, 0, origin.analytic_tag_ids.ids)] } if self.reservation_id: vals.update({ 'name': self.description + ' (' + self.description_dates + ')', 'invoice_id': invoice_id, 'reservation_ids': [(6, 0, [origin.id])], 'reservation_line_ids': [(6, 0, self.reservation_line_ids.ids)] }) elif self.service_id: vals.update({ 'name': self.description, 'invoice_id': invoice_id, 'service_ids': [(6, 0, [origin.id])] }) invoice_lines |= self.env['account.invoice.line'].create(vals) return invoice_lines