[IMP] sale_timesheet: Invoicing wizard improvements
Purpose of this task is, the invoicing wizard is being able to define a period with a start and an end date would allow a bit more flexibility. Currently, the invoicing wizard allows to invoice timesheets up to a certain date.It is links all timesheets before or equals this date in the sale order are invoiced. In this commit, we add another date field so that the user can define a period with a start and an end date and rename the 'invoice timesheets up to' field into 'Timesheets Period'. Also, use widget 'Daterange' in both fields and update tooltips accordingly this fields. By default we leave this two field empty. After this commit behaviour of invoicing wizard: -If these fields are empty, include all timesheets without distinction in the invoice -If these fields are set, only include the timesheets that fall within this period in the invoice -If applicable, only include approved timesheets TaskId:2252798 Closes: #52073
This commit is contained in:
committed by
Barad Mahendra
parent
e249d2b434
commit
4cc8fe2a98
@@ -43,21 +43,23 @@ class AccountMove(models.Model):
|
||||
}
|
||||
}
|
||||
|
||||
def _link_timesheets_to_invoice(self, date=None):
|
||||
""" Search timesheets and link this timesheets to the invoice
|
||||
def _link_timesheets_to_invoice(self, start_date=None, end_date=None):
|
||||
""" Search timesheets from given period and link this timesheets to the invoice
|
||||
|
||||
When we create an invoice from a sale order, we need to
|
||||
link the timesheets in this sale order to the invoice.
|
||||
Then, we can know which timesheets are invoiced in the sale order.
|
||||
:param date: All timesheets on task before or equals this date
|
||||
in the sale order are invoiced.
|
||||
:param start_date: the start date of the period
|
||||
:param end_date: the end date of the period
|
||||
"""
|
||||
for line in self.filtered(lambda i: i.move_type == 'out_invoice' and i.state == 'draft').invoice_line_ids:
|
||||
sale_line_delivery = line.sale_line_ids.filtered(lambda sol: sol.product_id.invoice_policy == 'delivery' and sol.product_id.service_type == 'timesheet')
|
||||
if sale_line_delivery:
|
||||
domain = line._timesheet_domain_get_invoiced_lines(sale_line_delivery)
|
||||
if date:
|
||||
domain = expression.AND([domain, [('date', '<=', date)]])
|
||||
if start_date:
|
||||
domain = expression.AND([domain, [('date', '>=', start_date)]])
|
||||
if end_date:
|
||||
domain = expression.AND([domain, [('date', '<=', end_date)]])
|
||||
timesheets = self.env['account.analytic.line'].sudo().search(domain)
|
||||
timesheets.write({'timesheet_invoice_id': line.move_id.id})
|
||||
|
||||
|
||||
@@ -60,14 +60,15 @@ class SaleOrder(models.Model):
|
||||
action = {'type': 'ir.actions.act_window_close'}
|
||||
return action
|
||||
|
||||
def _create_invoices(self, grouped=False, final=False, date=None):
|
||||
def _create_invoices(self, grouped=False, final=False, start_date=None, end_date=None):
|
||||
""" Override the _create_invoice method in sale.order model in sale module
|
||||
Add new parameter in this method, to invoice sale.order with a date. This date is used in sale_make_invoice_advance_inv into this module.
|
||||
:param date
|
||||
:param start_date: the start date of the period
|
||||
:param end_date: the end date of the period
|
||||
:return {account.move}: the invoices created
|
||||
"""
|
||||
moves = super(SaleOrder, self)._create_invoices(grouped, final)
|
||||
moves._link_timesheets_to_invoice(date)
|
||||
moves._link_timesheets_to_invoice(start_date, end_date)
|
||||
return moves
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
@@ -115,22 +116,26 @@ class SaleOrderLine(models.Model):
|
||||
project.write({'allow_timesheets': True})
|
||||
return project
|
||||
|
||||
def _recompute_qty_to_invoice(self, date):
|
||||
def _recompute_qty_to_invoice(self, start_date, end_date):
|
||||
""" Recompute the qty_to_invoice field for product containing timesheets
|
||||
|
||||
Search the existed timesheets up the given date in parameter.
|
||||
Search the existed timesheets between the given period in parameter.
|
||||
Retrieve the unit_amount of this timesheet and then recompute
|
||||
the qty_to_invoice for each current product.
|
||||
|
||||
:param date: date to search timesheets before this date.
|
||||
:param start_date: the start date of the period
|
||||
:param end_date: the end date of the period
|
||||
"""
|
||||
lines_by_timesheet = self.filtered(lambda sol: sol.product_id._is_delivered_timesheet())
|
||||
domain = lines_by_timesheet._timesheet_compute_delivered_quantity_domain()
|
||||
domain = expression.AND([domain, [
|
||||
('date', '<=', date),
|
||||
'|',
|
||||
('timesheet_invoice_id', '=', False),
|
||||
('timesheet_invoice_id.state', '=', 'cancel')]])
|
||||
if start_date:
|
||||
domain = expression.AND([domain, [('date', '>=', start_date)]])
|
||||
if end_date:
|
||||
domain = expression.AND([domain, [('date', '<=', end_date)]])
|
||||
mapping = lines_by_timesheet.sudo()._get_delivered_quantity_by_analytic(domain)
|
||||
|
||||
for line in lines_by_timesheet:
|
||||
|
||||
@@ -467,22 +467,24 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
# invoice SO
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
'date_invoice_timesheet': today - timedelta(days=10)
|
||||
'date_start_invoice_timesheet': today - timedelta(days=16),
|
||||
'date_end_invoice_timesheet': today - timedelta(days=10)
|
||||
})
|
||||
|
||||
self.assertTrue(wizard.invoicing_timesheet_enabled, 'The "date_invoice_timesheet" field should be visible in the wizard because a product in sale order has service_policy to "Timesheet on Task"')
|
||||
self.assertTrue(wizard.invoicing_timesheet_enabled, 'The "date_start_invoice_timesheet" and "date_end_invoice_timesheet" field should be visible in the wizard because a product in sale order has service_policy to "Timesheet on Task"')
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
wizard.create_invoices()
|
||||
|
||||
self.assertFalse(sale_order.invoice_ids, 'Normally, no invoice will be created created because the timesheet logged is after the date defined in date_invoice_timesheet field')
|
||||
self.assertFalse(sale_order.invoice_ids, 'Normally, no invoice will be created because the timesheet logged is after the period defined in date_start_invoice_timesheet and date_end_invoice_timesheet field')
|
||||
|
||||
wizard.write({
|
||||
'date_invoice_timesheet': today - timedelta(days=6)
|
||||
'date_start_invoice_timesheet': today - timedelta(days=10),
|
||||
'date_end_invoice_timesheet': today - timedelta(days=6)
|
||||
})
|
||||
wizard.create_invoices()
|
||||
|
||||
self.assertTrue(sale_order.invoice_ids, 'One invoice should be created because the timesheet logged is before or equal to the date defined in wizard')
|
||||
self.assertTrue(sale_order.invoice_ids, 'One invoice should be created because the timesheet logged is between the period defined in wizard')
|
||||
|
||||
invoice = sale_order.invoice_ids[0]
|
||||
self.assertEqual(so_line_deliver_global_project.qty_invoiced, timesheet1.unit_amount)
|
||||
@@ -491,17 +493,19 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
invoice.action_post()
|
||||
|
||||
wizard.write({
|
||||
'date_invoice_timesheet': today - timedelta(days=4)
|
||||
'date_start_invoice_timesheet': today - timedelta(days=16),
|
||||
'date_end_invoice_timesheet': today - timedelta(days=4)
|
||||
})
|
||||
wizard.create_invoices()
|
||||
|
||||
self.assertEqual(len(sale_order.invoice_ids), 2)
|
||||
invoice2 = sale_order.invoice_ids[-1]
|
||||
|
||||
self.assertEqual(so_line_deliver_global_project.qty_invoiced, timesheet1.unit_amount + timesheet3.unit_amount, "The last invoice done should have the quantity of the timesheet 3, because the date this timesheet is the only one before the 'date_invoice_timesheet' field in the wizard.")
|
||||
self.assertEqual(so_line_deliver_global_project.qty_invoiced, timesheet1.unit_amount + timesheet3.unit_amount, "The last invoice done should have the quantity of the timesheet 3, because the date this timesheet is the only one before the 'date_end_invoice_timesheet' field in the wizard.")
|
||||
|
||||
wizard.write({
|
||||
'date_invoice_timesheet': today
|
||||
'date_start_invoice_timesheet': today - timedelta(days=4),
|
||||
'date_end_invoice_timesheet': today
|
||||
})
|
||||
|
||||
wizard.create_invoices()
|
||||
@@ -565,7 +569,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
}
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
'date_invoice_timesheet': today
|
||||
'date_start_invoice_timesheet': today - timedelta(days=4),
|
||||
'date_end_invoice_timesheet': today
|
||||
})
|
||||
wizard.create_invoices()
|
||||
|
||||
|
||||
@@ -16,18 +16,19 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
product_ids = order_lines.mapped('product_id').filtered(lambda p: p._is_delivered_timesheet())
|
||||
return bool(product_ids)
|
||||
|
||||
date_invoice_timesheet = fields.Date(
|
||||
string='Invoice Timesheets Up To This Date',
|
||||
default=fields.Date.context_today,
|
||||
required=False,
|
||||
help="Only timesheets not yet invoiced (and validated, if applicable) up to this date included will be invoiced. If no date is indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction.")
|
||||
date_start_invoice_timesheet = fields.Date(
|
||||
string='Start Date',
|
||||
help="Only timesheets not yet invoiced (and validated, if applicable) from this period will be invoiced. If the period is not indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction.")
|
||||
date_end_invoice_timesheet = fields.Date(
|
||||
string='End Date',
|
||||
help="Only timesheets not yet invoiced (and validated, if applicable) from this period will be invoiced. If the period is not indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction.")
|
||||
invoicing_timesheet_enabled = fields.Boolean(default=_default_invoicing_timesheet_enabled)
|
||||
|
||||
def create_invoices(self):
|
||||
""" Override method from sale/wizard/sale_make_invoice_advance.py
|
||||
|
||||
When the user want to invoice the timesheets to the SO
|
||||
up to a specific date then we need to recompute the
|
||||
up to a specific period then we need to recompute the
|
||||
qty_to_invoice for each product_id in sale.order.line,
|
||||
before creating the invoice.
|
||||
"""
|
||||
@@ -36,10 +37,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
||||
)
|
||||
|
||||
if self.advance_payment_method == 'delivered' and self.invoicing_timesheet_enabled:
|
||||
if self.date_invoice_timesheet:
|
||||
sale_orders.mapped('order_line')._recompute_qty_to_invoice(self.date_invoice_timesheet)
|
||||
if self.date_start_invoice_timesheet or self.date_end_invoice_timesheet:
|
||||
sale_orders.mapped('order_line')._recompute_qty_to_invoice(self.date_start_invoice_timesheet, self.date_end_invoice_timesheet)
|
||||
|
||||
sale_orders._create_invoices(final=self.deduct_down_payments, date=self.date_invoice_timesheet)
|
||||
sale_orders._create_invoices(final=self.deduct_down_payments, start_date=self.date_start_invoice_timesheet, end_date=self.date_end_invoice_timesheet)
|
||||
|
||||
if self._context.get('open_invoices', False):
|
||||
return sale_orders.action_view_invoice()
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
<field name="inherit_id" ref="sale.view_sale_advance_payment_inv"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='deposit_taxes_id']" position="after">
|
||||
<field name="date_invoice_timesheet" class="oe_inline" string="Invoice Timesheets up to" data-toggle="tooltip" data-placement="bottom"
|
||||
title="Only timesheets not yet invoiced (and validated, if applicable) up to this date included will be invoiced. If no date is indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction."
|
||||
attrs="{
|
||||
'invisible': [ '|',
|
||||
('invoicing_timesheet_enabled', '=', False),
|
||||
('advance_payment_method', '!=', 'delivered'),
|
||||
]
|
||||
}"
|
||||
/>
|
||||
<field name="invoicing_timesheet_enabled" invisible="1"/>
|
||||
<label for="date_start_invoice_timesheet" string="Timesheets Period" attrs="{'invisible': [ '|', ('invoicing_timesheet_enabled', '=', False), ('advance_payment_method', '!=', 'delivered')]}"/>
|
||||
<div clas="o_row" attrs="{'invisible': [ '|',('invoicing_timesheet_enabled', '=', False), ('advance_payment_method', '!=', 'delivered')]}">
|
||||
<field name="date_start_invoice_timesheet"
|
||||
class="oe_inline" widget="daterange"
|
||||
options="{'related_end_date': 'date_end_invoice_timesheet'}"
|
||||
title="Only timesheets not yet invoiced (and validated, if applicable) from this period will be invoiced. If the period is not indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction."/>
|
||||
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow"/>
|
||||
<field name="date_end_invoice_timesheet"
|
||||
class="oe_inline" widget="daterange"
|
||||
options="{'related_start_date': 'date_start_invoice_timesheet'}"
|
||||
title="Only timesheets not yet invoiced (and validated, if applicable) from this period will be invoiced. If the period is not indicated, all timesheets not yet invoiced (and validated, if applicable) will be invoiced without distinction."/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Reference in New Issue
Block a user