[WIP] Invoice WorkFlow

This commit is contained in:
Dario Lodeiros
2019-01-03 19:03:57 +01:00
parent 12ca208b59
commit 8ba2051496
9 changed files with 357 additions and 47 deletions

View File

@@ -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

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)

View File

@@ -20,9 +20,9 @@
attrs="{'invisible': [('has_cancelled_reservations_to_send', '=', False)]}" class="oe_highlight"/> -->
<!-- <button name="send_exit_mail" type="object" string="Send Exit Email"
attrs="{'invisible': [('has_checkout_to_send', '=', False)]}" class="oe_highlight"/> -->
<!-- <button name="%(hotel.action_view_folio_advance_payment_inv)d"
<button name="%(hotel.action_view_folio_advance_payment_inv)d"
string="Create Invoice" type="action" class="btn-primary" states="sale"
attrs="{'invisible': [('invoice_status', '!=', 'to invoice')]}"/> -->
attrs="{'invisible': [('state', '!=', 'confirm')]}"/>
<!-- <button name="action_cancel_draft" states="cancel,sale" string="Set to Draft"
type="object" icon="fa-undo" class="oe_highlight" /> -->
<button name="action_cancel" string="Cancel Folio" states="sale"
@@ -132,10 +132,12 @@
<field name="email" placeholder="email"/>
<field name="mobile" placeholder="mobile"/>
<field name="phone" />
<field name="partner_invoice_id" />
<field name="cancelled_reason" attrs="{'invisible':[('state','not in',('cancel'))]}"/>
</group>
<group>
<field name="pricelist_id" domain="[('type','=','sale')]" />
<field name="company_id" />
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="reservation_type" attrs="{'readonly':[('state','not in',('draft'))]}"/>
<field name="channel_type" attrs="{'required':[('reservation_type','=','normal')]}"/>

View File

@@ -16,14 +16,7 @@ class FolioAdvancePaymentInv(models.TransientModel):
@api.model
def _get_advance_payment_method(self):
if self._count() == 1:
sale_obj = self.env['sale.order']
folio_obj = self.env['hotel.folio']
order = sale_obj.browse(folio_obj.mapped('order_id.id'))
if all([line.product_id.invoice_policy == 'order' for line in order.order_line]) \
or order.invoice_count:
return 'all'
return 'delivered'
return 'all'
@api.model
def _default_product_id(self):
@@ -40,15 +33,22 @@ class FolioAdvancePaymentInv(models.TransientModel):
return self._default_product_id().taxes_id
advance_payment_method = fields.Selection([
('delivered', 'Invoiceable lines'),
('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)
count = fields.Integer(default=_count, string='# of Orders')
folio_ids = fields.Many2many("hotel.folio", string="Folios",
help="Folios grouped")
group_folios = fields.Boolean('Group Folios')
line_ids = fields.One2many('line.advance.inv',
'advance_inv_id',
string="Invoice Lines")
view_detail = fields.Boolean('View Detail')
#Advance Payment
product_id = fields.Many2one('product.product', string='Down Payment Product',
domain=[('type', '=', 'service')], default=_default_product_id)
count = fields.Integer(default=_count, string='# of Orders')
amount = fields.Float('Down Payment Amount',
digits=dp.get_precision('Account'),
help="The amount to be invoiced in advance, taxes excluded.")
@@ -139,12 +139,11 @@ class FolioAdvancePaymentInv(models.TransientModel):
@api.multi
def create_invoices(self):
folios = self.env['hotel.folio'].browse(self._context.get('active_ids', []))
sale_orders = self.env['sale.order'].browse(folios.mapped('order_id.id'))
if self.advance_payment_method == 'delivered':
sale_orders.action_invoice_create()
folios.action_invoice_create()
elif self.advance_payment_method == 'all':
sale_orders.action_invoice_create(final=True)
folios.action_invoice_create()
else:
# Create deposit product if necessary
if not self.product_id:
@@ -196,3 +195,68 @@ class FolioAdvancePaymentInv(models.TransientModel):
'property_account_income_id': self.deposit_account_id.id,
'taxes_id': [(6, 0, self.deposit_taxes_id.ids)],
}
@api.onchange('view_detail')
def prepare_reservation_invoice_lines(self):
vals = []
folios = self.env['hotel.folio'].browse(self._context.get('active_ids', []))
for folio in folios:
folio_name = folio.name
for reservation in folio.room_lines:
reservation_name = reservation.name
unit_price = False
discount = False
qty = 0
for day in reservation.reservation_line_ids.sorted('date'):
if day.price == unit_price and day.discount == discount:
date_to = day.date
qty += 1
else:
if unit_price:
vals.append((0, False, {
'date_from': date_from,
'date_to': date_to,
'room_type_id': reservation.room_type_id,
'product_id': self.env['product.product'].browse(
reservation.room_type_id.product_id.id
),
'qty': qty,
'discount': discount,
'unit_price': unit_price
}))
qty = 1
unit_price = day.price
date_from = day.date
date_to = day.date
vals.append((0, False, {
'date_from': date_from,
'date_to': date_to,
'room_type_id': reservation.room_type_id,
'product_id': self.env['product.product'].browse(
reservation.room_type_id.product_id.id
),
'qty': qty,
'discount': discount,
'unit_price': unit_price
}))
self.line_ids = vals
class LineAdvancePaymentInv(models.TransientModel):
_name = "line.advance.inv"
_description = "Lines Advance Invoice"
date_from = fields.Date('From')
date_to = fields.Date('To')
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')
unit_price = fields.Float('Price')
advance_inv_id = fields.Many2one('folio.advance.payment.inv')
discount = fields.Float(
string='Discount (%)',
digits=dp.get_precision('Discount'), default=0.0)
to_invoice = fields.Boolean('To Invoice')

View File

@@ -28,8 +28,23 @@
<field name="deposit_taxes_id" class="oe_inline" widget="many2many_tags"
domain="[('type_tax_use','=','sale')]"
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"/>
<field name="view_detail" />
</group>
<group>
<field name="line_ids" attrs="{'invisible': [('view_detail', '=', False)]}">
<tree string="Lines" editable="bottom">
<field name="room_type_id" />
<field name="date_from" />
<field name="date_to" />
<field name="qty" />
<field name="discount" />
<field name="unit_price" />
</tree>
</field>
</group>
<footer>
<button name="prepare_reservation_invoice_lines" string="Prepare Lines" type="object"
class="btn-primary"/>
<button name="create_invoices" string="Create and View Invoices" type="object"
context="{'open_invoices': True}" class="btn-primary"/>
<button name="create_invoices" string="Create Invoices" type="object"