Permalink
Browse files

[MERGE][IMP] sale: improve usability of down payments

Task #1908673

Purpose
=======

The "invoice" modal is too complex to use for new users and has a lot of useless options for
the most simple use cases.

The goal here is to remove 2 options of the 'advance_payment_method' field:
* Invoiceable lines
* Invoiceable lines (deduct down payments)

And combine them into a new one: "Standard invoice".

If there are any down payments to deduct, another checkbox field appears under this
one: "Deduct down payments".

This keeps all the invoicing features while simplifying the modal.

Spec
=======
* Remove "invoiceable lines, deduct down payment" and replace by a checkbox
  (checked by default, only visible if down payments to deduct)
* Rename "Invoiceable lines" into "Standard invoice"
* tooltip on radio buttons: A standard invoice is issued with all the order lines ready for invoicing,
  according to their invoicing policy (based on ordered quantity or on delivered quantity).

closes #29978
  • Loading branch information...
robodoo committed Feb 11, 2019
2 parents 50a41c9 + 5cec9cf commit 768844040f887868759309ad101fe50191fc949a
@@ -47,7 +47,7 @@ def test_01_delivery_stock_move(self):

# I confirm the SO.
self.sale_prepaid.action_confirm()
self.sale_prepaid.action_invoice_create()
self.sale_prepaid._create_invoices()

# I check that the invoice was created
self.assertEqual(len(self.sale_prepaid.invoice_ids), 1, "Invoice not created.")
@@ -274,15 +274,15 @@ def print_repair_order(self):

def action_repair_invoice_create(self):
for repair in self:
repair.action_invoice_create()
repair._create_invoices()
if repair.invoice_method == 'b4repair':
repair.action_repair_ready()
elif repair.invoice_method == 'after_repair':
repair.write({'state': 'done'})
return True

@api.multi
def action_invoice_create(self, group=False):
def _create_invoices(self, group=False):
""" Creates invoice(s) for repair order.
@param group: It is set to true when group invoice is to be generated.
@return: Invoice Ids.
@@ -17,10 +17,10 @@ def make_invoices(self):
new_invoice = {}
for wizard in self:
repairs = self.env['repair.order'].browse(self._context['active_ids'])
new_invoice = repairs.action_invoice_create(group=wizard.group)
new_invoice = repairs._create_invoices(group=wizard.group)

# We have to udpate the state of the given repairs, otherwise they remain 'to be invoiced'.
# Note that this will trigger another call to the method 'action_invoice_create',
# Note that this will trigger another call to the method '_create_invoices',
# but that second call will not do anything, since the repairs are already invoiced.
repairs.action_repair_invoice_create()
return {
@@ -114,7 +114,7 @@ def _invoice_sale_orders(self):
for trans in self.filtered(lambda t: t.sale_order_ids):
trans = trans.with_context(ctx_company)
trans.sale_order_ids._force_lines_to_invoice_policy_order()
invoices = trans.sale_order_ids.action_invoice_create()
invoices = trans.sale_order_ids._create_invoices()
trans.invoice_ids = [(6, 0, invoices)]

@api.model
@@ -485,7 +485,7 @@ def action_view_invoice(self):
return action

@api.multi
def action_invoice_create(self, grouped=False, final=False):
def _create_invoices(self, grouped=False, final=False):
"""
Create the invoice associated to the SO.
:param grouped: if True, invoices are grouped by SO id. If False, invoices are grouped by
@@ -97,7 +97,7 @@ def test_sale_order(self):
self.assertTrue(self.sale_order.invoice_status == 'to invoice')

# create invoice: only 'invoice on order' products are invoiced
inv_id = self.sale_order.action_invoice_create()
inv_id = self.sale_order._create_invoices()
invoice = Invoice.browse(inv_id)
self.assertEqual(len(invoice.invoice_line_ids), 2, 'Sale: invoice is missing lines')
self.assertEqual(invoice.amount_total, sum([2 * p.list_price if p.invoice_policy == 'order' else 0 for p in self.product_map.values()]), 'Sale: invoice total amount is wrong')
@@ -110,7 +110,7 @@ def test_sale_order(self):
for line in self.sale_order.order_line:
line.qty_delivered = 2 if line.product_id.expense_policy == 'no' else 0
self.assertTrue(self.sale_order.invoice_status == 'to invoice', 'Sale: SO status after delivery should be "to invoice"')
inv_id = self.sale_order.action_invoice_create()
inv_id = self.sale_order._create_invoices()
invoice2 = Invoice.browse(inv_id)
self.assertEqual(len(invoice2.invoice_line_ids), 2, 'Sale: second invoice is missing lines')
self.assertEqual(invoice2.amount_total, sum([2 * p.list_price if p.invoice_policy == 'delivery' else 0 for p in self.product_map.values()]), 'Sale: second invoice total amount is wrong')
@@ -124,7 +124,7 @@ def test_sale_order(self):
# upsell and invoice
self.sol_serv_order.write({'product_uom_qty': 10})

inv_id = self.sale_order.action_invoice_create()
inv_id = self.sale_order._create_invoices()
invoice3 = Invoice.browse(inv_id)
self.assertEqual(len(invoice3.invoice_line_ids), 1, 'Sale: third invoice is missing lines')
self.assertEqual(invoice3.amount_total, 8 * self.product_map['serv_order'].list_price, 'Sale: second invoice total amount is wrong')
@@ -92,7 +92,6 @@ def test_downpayment(self):

# Let's do an invoice with refunds
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
'advance_payment_method': 'all',
'deposit_account_id': self.account_income.id
})
payment.create_invoices()
@@ -38,7 +38,7 @@ def test_sale_invoicing_from_transaction(self):
self.assertTrue(transaction.payment_id)
self.assertEqual(transaction.payment_id.state, 'posted')

invoice_ids = order.action_invoice_create()
invoice_ids = order._create_invoices()
invoice = self.env['account.invoice'].browse(invoice_ids)
invoice.action_invoice_open()

@@ -16,17 +16,6 @@ class SaleAdvancePaymentInv(models.TransientModel):
def _count(self):
return len(self._context.get('active_ids', []))

@api.model
def _get_advance_payment_method(self):
if self._count() == 1:
sale_obj = self.env['sale.order']
order = sale_obj.browse(self._context.get('active_ids'))[0]
if order.order_line.filtered(lambda dp: dp.is_downpayment) and order.invoice_ids.filtered(lambda invoice: invoice.state != 'cancel') or order.order_line.filtered(lambda l: l.qty_to_invoice < 0):
return 'all'
else:
return 'delivered'
return 'all'

@api.model
def _default_product_id(self):
product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id')
@@ -40,12 +29,25 @@ def _default_deposit_account_id(self):
def _default_deposit_taxes_id(self):
return self._default_product_id().taxes_id

@api.model
def _default_has_down_payment(self):
if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False):
sale_order = self.env['sale.order'].browse(self._context.get('active_id'))
return sale_order.order_line.filtered(
lambda sale_order_line: sale_order_line.is_downpayment
)

return False

advance_payment_method = fields.Selection([
('delivered', 'Invoiceable lines'),
('all', 'Invoiceable lines (deduct down payments)'),
('delivered', 'Standard invoice'),
('percentage', 'Down payment (percentage)'),
('fixed', 'Down payment (fixed amount)')
], string='What do you want to invoice?', default=_get_advance_payment_method, required=True)
], string='What do you want to invoice?', default='delivered', required=True,
help="A standard invoice is issued with all the order lines ready for invoicing, \
according to their invoicing policy (based on ordered or delivered quantity).")
deduct_down_payments = fields.Boolean('Deduct down payments', default=True)
has_down_payments = fields.Boolean('Has down payments', default=_default_has_down_payment, readonly=True)
product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')],
default=_default_product_id)
count = fields.Integer(default=_count, string='Order Count')
@@ -132,9 +134,7 @@ def create_invoices(self):
sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', []))

if self.advance_payment_method == 'delivered':
sale_orders.action_invoice_create()
elif self.advance_payment_method == 'all':
sale_orders.action_invoice_create(final=True)
sale_orders._create_invoices(final=self.deduct_down_payments)
else:
# Create deposit product if necessary
if not self.product_id:
@@ -13,6 +13,9 @@
<field name="count" invisible="[('count','=',1)]" readonly="True"/>
<field name="advance_payment_method" class="oe_inline" widget="radio"
attrs="{'invisible': [('count','&gt;',1)]}"/>
<field name="has_down_payments" invisible="1" />
<field name="deduct_down_payments"
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}" />
<field name="product_id"
context="{'search_default_services': 1, 'default_type': 'service', 'default_invoice_policy': 'order'}" class="oe_inline"
invisible="1"/>
@@ -30,10 +33,9 @@
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"/>
</group>
<footer>
<button name="create_invoices" string="Create and View Invoices" type="object"
<button name="create_invoices" string="Create and View Invoice" type="object"
context="{'open_invoices': True}" class="btn-primary"/>
<button name="create_invoices" string="Create Invoices" type="object"
class="btn-primary"/>
<button name="create_invoices" string="Create Invoice" type="object"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
@@ -88,6 +88,6 @@ def test_sale_expense(self):
# self.assertTrue(so.invoice_status, 'no', 'Sale Expense: expenses should not impact the invoice_status of the so')

# both expenses should be invoiced
inv_id = so.action_invoice_create()
inv_id = so._create_invoices()
inv = self.env['account.invoice'].browse(inv_id)
self.assertEqual(inv.amount_untaxed, 621.54 + (prod_exp_2.list_price * 100.0), 'Sale Expense: invoicing of expense is wrong')
@@ -515,7 +515,7 @@ def test_01_sale_mrp_delivery_kit(self):

# invoice in on delivery, nothing should be invoiced
with self.assertRaises(UserError):
so.action_invoice_create()
so._create_invoices()
self.assertEqual(so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "nothing to invoice" after invoicing')

# deliver partially (1 of each instead of 5), check the so's invoice_status and delivered quantities
@@ -636,7 +636,7 @@ def test_02_sale_mrp_anglo_saxon(self):
wiz = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz.process()
# Create the invoice
self.so.action_invoice_create()
self.so._create_invoices()
self.invoice = self.so.invoice_ids
# Changed the invoiced quantity of the finished product to 2
self.invoice.invoice_line_ids.write({'quantity': 2.0})
@@ -27,7 +27,7 @@ def test_00_sale_stock_invoice(self):
self.so.action_confirm()
self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" storable products')
# invoice on order
self.so.action_invoice_create()
self.so._create_invoices()

# deliver partially, check the so's invoice_status and delivered quantities
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "nothing to invoice" after invoicing')
@@ -41,7 +41,7 @@ def test_00_sale_stock_invoice(self):
del_qties_truth = [1.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line]
self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery')
# invoice on delivery: only storable products
inv_id = self.so.action_invoice_create()
inv_id = self.so._create_invoices()
inv_1 = inv_obj.browse(inv_id)
self.assertTrue(all([il.product_id.invoice_policy == 'delivery' for il in inv_1.invoice_line_ids]),
'Sale Stock: invoice should only contain "invoice on delivery" products')
@@ -59,7 +59,7 @@ def test_00_sale_stock_invoice(self):
self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after complete delivery')
# Without timesheet, we manually set the delivered qty for the product serv_del
self.so.order_line[1]['qty_delivered'] = 2.0
inv_id = self.so.action_invoice_create()
inv_id = self.so._create_invoices()
self.assertEqual(self.so.invoice_status, 'invoiced',
'Sale Stock: so invoice_status should be "fully invoiced" after complete delivery and invoicing')

@@ -97,7 +97,7 @@ def test_01_sale_stock_order(self):
self.assertEqual(inv.amount_untaxed, self.so.amount_untaxed * 5.0 / 100.0, 'Sale Stock: deposit invoice is wrong')
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so should be to invoice after invoicing deposit')
# invoice on order: everything should be invoiced
self.so.action_invoice_create(final=True)
self.so._create_invoices(final=True)
self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so should be fully invoiced after second invoice')

# deliver, check the delivered quantities
@@ -109,7 +109,7 @@ def test_01_sale_stock_order(self):
self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery')
# invoice on delivery: nothing to invoice
with self.assertRaises(UserError):
self.so.action_invoice_create()
self.so._create_invoices()

def test_02_sale_stock_return(self):
"""
@@ -151,7 +151,7 @@ def test_02_sale_stock_return(self):

# Check invoice
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" before invoicing' % self.so.invoice_status)
inv_1_id = self.so.action_invoice_create()
inv_1_id = self.so._create_invoices()
self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so invoice_status should be "invoiced" instead of "%s" after invoicing' % self.so.invoice_status)
self.assertEqual(len(inv_1_id), 1, 'Sale Stock: only one invoice instead of "%s" should be created' % len(inv_1_id))
self.inv_1 = self.env['account.invoice'].browse(inv_1_id)
@@ -176,7 +176,7 @@ def test_02_sale_stock_return(self):
self.assertAlmostEqual(self.so.order_line[0].qty_delivered, 3.0, msg='Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered)
# let's do an invoice with refunds
adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({
'advance_payment_method': 'all',
'advance_payment_method': 'delivered',
})
adv_wiz.with_context(open_invoices=True).create_invoices()
self.inv_2 = self.so.invoice_ids.filtered(lambda r: r.state == 'draft')
@@ -225,7 +225,7 @@ def test_03_sale_stock_delivery_partial(self):

# Check invoice
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" before invoicing')
inv_1_id = self.so.action_invoice_create()
inv_1_id = self.so._create_invoices()
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" after invoicing')
self.assertEqual(len(inv_1_id), 1, 'Sale Stock: only one invoice should be created')
self.inv_1 = self.env['account.invoice'].browse(inv_1_id)
@@ -58,7 +58,7 @@ def test_sale_service(self):
'employee_id': self.employee_manager.id,
})
self.assertEqual(self.sale_order.invoice_status, 'to invoice', 'Sale Service: there should be sale_ordermething to invoice after registering timesheets')
self.sale_order.action_invoice_create()
self.sale_order._create_invoices()

self.assertTrue(sale_order_line.product_uom_qty == sale_order_line.qty_delivered == sale_order_line.qty_invoiced, 'Sale Service: line should be invoiced completely')
self.assertEqual(self.sale_order.invoice_status, 'invoiced', 'Sale Service: SO should be invoiced')
@@ -126,7 +126,7 @@ def test_timesheet_uom(self):
'unit_amount': 24,
'employee_id': self.employee_user.id,
})
self.sale_order.action_invoice_create()
self.sale_order._create_invoices()
self.assertEqual(self.sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')

def test_task_so_line_assignation(self):
@@ -168,7 +168,7 @@ def test_task_so_line_assignation(self):
self.assertEqual(task_serv2.timesheet_ids.mapped('so_line'), so_line_deliver_global_project, "Old timesheet are not modified when changing the task SO line")

# invoice SO, and validate invoice
invoice_id = self.sale_order.action_invoice_create()[0]
invoice_id = self.sale_order._create_invoices()[0]
invoice = self.env['account.invoice'].browse(invoice_id)
invoice.action_invoice_open()

Oops, something went wrong.

0 comments on commit 7688440

Please sign in to comment.