diff --git a/addons/sale_stock/models/account_move.py b/addons/sale_stock/models/account_move.py index 9c34d5bb955e9ce9db2e151b737c49d9bc7d10fc..26513f9ccc543b4f181e02f00763cc9d829ba45d 100644 --- a/addons/sale_stock/models/account_move.py +++ b/addons/sale_stock/models/account_move.py @@ -33,11 +33,23 @@ class AccountMove(models.Model): all_invoices_amls = current_invoice_amls.sale_line_ids.invoice_lines.filtered(lambda aml: aml.move_id.state == 'posted').sorted(lambda aml: (aml.date, aml.move_name, aml.id)) index = all_invoices_amls.ids.index(current_invoice_amls[:1].id) if current_invoice_amls[:1] in all_invoices_amls else 0 previous_amls = all_invoices_amls[:index] - - previous_qties_invoiced = previous_amls._get_invoiced_qty_per_product() invoiced_qties = current_invoice_amls._get_invoiced_qty_per_product() invoiced_products = invoiced_qties.keys() + if self.move_type == 'out_invoice': + # filter out the invoices that have been fully refund and re-invoice otherwise, the quantities would be + # consumed by the reversed invoice and won't be print on the new draft invoice + previous_amls = previous_amls.filtered(lambda aml: aml.move_id.payment_state != 'reversed') + + previous_qties_invoiced = previous_amls._get_invoiced_qty_per_product() + + if self.move_type == 'out_refund': + # we swap the sign because it's a refund, and it would print negative number otherwise + for p in previous_qties_invoiced: + previous_qties_invoiced[p] = -previous_qties_invoiced[p] + for p in invoiced_qties: + invoiced_qties[p] = -invoiced_qties[p] + qties_per_lot = defaultdict(float) previous_qties_delivered = defaultdict(float) stock_move_lines = current_invoice_amls.sale_line_ids.move_ids.move_line_ids.filtered(lambda sml: sml.state == 'done' and sml.lot_id).sorted(lambda sml: (sml.date, sml.id)) @@ -48,7 +60,13 @@ class AccountMove(models.Model): product_uom = product.uom_id qty_done = sml.product_uom_id._compute_quantity(sml.qty_done, product_uom) - if sml.location_id.usage == 'customer': + # is it a stock return considering the document type (should it be it thought of as positively or negatively?) + is_stock_return = ( + self.move_type == 'out_invoice' and (sml.location_id.usage, sml.location_dest_id.usage) == ('customer', 'internal') + or + self.move_type == 'out_refund' and (sml.location_id.usage, sml.location_dest_id.usage) == ('internal', 'customer') + ) + if is_stock_return: returned_qty = min(qties_per_lot[sml.lot_id], qty_done) qties_per_lot[sml.lot_id] -= returned_qty qty_done = returned_qty - qty_done @@ -60,7 +78,7 @@ class AccountMove(models.Model): # try to reach the previous_qty_invoiced if float_compare(qty_done, 0, precision_rounding=product_uom.rounding) < 0 or \ float_compare(previous_qty_delivered, previous_qty_invoiced, precision_rounding=product_uom.rounding) < 0: - previously_done = qty_done if sml.location_id.usage == 'customer' else min(previous_qty_invoiced - previous_qty_delivered, qty_done) + previously_done = qty_done if is_stock_return else min(previous_qty_invoiced - previous_qty_delivered, qty_done) previous_qties_delivered[product] += previously_done qty_done -= previously_done diff --git a/addons/sale_stock/tests/test_report.py b/addons/sale_stock/tests/test_report.py index c4513d8c84784e04b4885d585eee6260b5f162e3..0a098d70232f347f595b757409ac97a8a54b83da 100644 --- a/addons/sale_stock/tests/test_report.py +++ b/addons/sale_stock/tests/test_report.py @@ -40,6 +40,8 @@ class TestSaleStockInvoices(TestSaleCommon): 'product_id': self.product_by_usn.id, 'company_id': self.env.company.id, }) + self.usn01 = usn01 + self.usn02 = usn02 self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 10, lot_id=lot) self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=usn01) self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=usn02) @@ -281,3 +283,109 @@ class TestSaleStockInvoices(TestSaleCommon): self.assertRegex(text, r'Product By Lot\n6.00\nUnits\nLOT0002', "There should be a line that specifies 6 x LOT0002") self.assertRegex(text, r'Product By Lot\n2.00\nUnits\nLOT0003', "There should be a line that specifies 2 x LOT0003") self.assertNotIn('LOT0001', text) + + def test_refund_cancel_invoices(self): + """ + Suppose the lots are printed on the invoices. + The user sells 2 tracked-by-usn products, he delivers 2 products and invoices them + Then he adds credit notes and issues a full refund. Receive the products. + The reversed invoice should also have correct USN + """ + + report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments') + display_lots = self.env.ref('sale_stock.group_lot_on_invoice') + display_uom = self.env.ref('uom.group_uom') + self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]}) + + so = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [ + (0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}), + ], + }) + so.action_confirm() + + picking = so.picking_ids + picking.move_lines.move_line_ids[0].qty_done = 1 + picking.move_lines.move_line_ids[1].qty_done = 1 + picking.button_validate() + + invoice01 = so._create_invoices() + invoice01.action_post() + + html = report._render_qweb_html(invoice01.ids)[0] + text = html2plaintext(html) + self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001") + self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0002', "There should be a line that specifies 1 x USN0002") + + # Refund the invoice + refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice01.id]).create({ + 'refund_method': 'cancel', + }) + refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id']) + + # recieve the returned product + stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(active_ids=picking.ids, active_id=picking.sorted().ids[0], active_model='stock.picking')) + return_wiz = stock_return_picking_form.save() + res = return_wiz.create_returns() + pick_return = self.env['stock.picking'].browse(res['res_id']) + + move_form = Form(pick_return.move_lines, view='stock.view_stock_move_nosuggest_operations') + with move_form.move_line_nosuggest_ids.new() as line: + line.lot_id = self.usn01 + line.qty_done = 1 + with move_form.move_line_nosuggest_ids.new() as line: + line.lot_id = self.usn02 + line.qty_done = 1 + move_form.save() + pick_return.button_validate() + + # reversed invoice + html = report._render_qweb_html(refund_invoice.ids)[0] + text = html2plaintext(html) + self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001") + self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0002', "There should be a line that specifies 1 x USN0002") + + def test_refund_modify_invoices(self): + """ + Suppose the lots are printed on the invoices. + The user sells 1 tracked-by-usn products, he delivers 1 and invoices it + Then he adds credit notes and issues full refund and new draft invoice. + The new draft invoice should have correct USN + """ + + report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments') + display_lots = self.env.ref('sale_stock.group_lot_on_invoice') + display_uom = self.env.ref('uom.group_uom') + self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]}) + + so = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [ + (0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 1}), + ], + }) + so.action_confirm() + + picking = so.picking_ids + picking.move_lines.move_line_ids[0].qty_done = 1 + picking.button_validate() + + invoice01 = so._create_invoices() + invoice01.action_post() + + html = report._render_qweb_html(invoice01.ids)[0] + text = html2plaintext(html) + self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001") + + # Refund the invoice with full refund and new draft invoice + refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice01.id]).create({ + 'refund_method': 'modify', + }) + invoice02 = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id']) + invoice02.action_post() + + # new draft invoice + html = report._render_qweb_html(invoice02.ids)[0] + text = html2plaintext(html) + self.assertRegex(text, r'Product By USN\n1.00\nUnits\nUSN0001', "There should be a line that specifies 1 x USN0001")