diff --git a/CHANGELOG b/CHANGELOG index 9f7592c109..a42e827a66 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Invoice shipped product instead of sold product + Version 4.6.0 - 2017-10-30 * Bug fixes (see mercurial logs for details) * Plan backorder moves to today diff --git a/sale.py b/sale.py index 61aba34889..25c28482ab 100644 --- a/sale.py +++ b/sale.py @@ -1,6 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime +from collections import defaultdict from decimal import Decimal from itertools import groupby, chain from functools import partial @@ -1344,54 +1345,83 @@ class SaleLine(sequence_ordered(), ModelSQL, ModelView): 'Return a list of invoice lines for sale line' pool = Pool() InvoiceLine = pool.get('account.invoice.line') + Product = pool.get('product.product') + Uom = pool.get('product.uom') AccountConfiguration = pool.get('account.configuration') account_config = AccountConfiguration(1) - invoice_line = InvoiceLine() - invoice_line.type = self.type - invoice_line.description = self.description - invoice_line.note = self.note - invoice_line.origin = self + def _invoice_line(): + invoice_line = InvoiceLine() + invoice_line.type = self.type + invoice_line.description = self.description + invoice_line.note = self.note + invoice_line.origin = self + return invoice_line if self.type != 'line': if self._get_invoice_not_line(): - return [invoice_line] + return [_invoice_line()] else: return [] quantity = (self._get_invoice_line_quantity() - self._get_invoiced_quantity()) - if self.unit: - quantity = self.unit.round(quantity) - invoice_line.quantity = quantity + stock_moves = self._get_invoice_line_moves() - if not invoice_line.quantity: - return [] - - invoice_line.unit = self.unit - invoice_line.product = self.product - invoice_line.unit_price = self.unit_price - invoice_line.taxes = self.taxes - invoice_line.invoice_type = 'out' - if self.product: - invoice_line.account = self.product.account_revenue_used - if not invoice_line.account: - self.raise_user_error('missing_account_revenue', { - 'sale': self.sale.rec_name, - 'product': self.product.rec_name, - }) + products_quantity = defaultdict(lambda: 0) + if stock_moves and any(m.product != self.product for m in stock_moves): + for move in stock_moves: + move_quantity = move.quantity - move.invoiced_quantity + products_quantity[move.product] += ( + Uom.compute_qty(move.uom, move_quantity, self.unit)) + assert sum(products_quantity.itervalues()) == quantity else: - for name in ['default_product_account_revenue', - 'default_category_account_revenue']: - invoice_line.account = account_config.get_multivalue(name) - if invoice_line.account: - break - if not invoice_line.account: - self.raise_user_error('missing_default_account_revenue', { - 'sale': self.sale.rec_name, - }) - invoice_line.stock_moves = self._get_invoice_line_moves() - return [invoice_line] + products_quantity[self.product] = quantity + + context = {} + if self.sale.party.lang: + context['language'] = self.sale.party.lang.code + + lines = [] + for product, quantity in products_quantity.iteritems(): + invoice_line = _invoice_line() + if self.unit: + quantity = self.unit.round(quantity) + invoice_line.quantity = quantity + + if not invoice_line.quantity: + continue + + if product != self.product: + with Transaction().set_context(context): + invoice_line.description = Product(product.id).rec_name + + invoice_line.unit = self.unit + invoice_line.product = product + invoice_line.unit_price = self.unit_price + invoice_line.taxes = self.taxes + invoice_line.invoice_type = 'out' + if product: + invoice_line.account = product.account_revenue_used + if not invoice_line.account: + self.raise_user_error('missing_account_revenue', { + 'sale': self.sale.rec_name, + 'product': product.rec_name, + }) + else: + for name in ['default_product_account_revenue', + 'default_category_account_revenue']: + invoice_line.account = account_config.get_multivalue(name) + if invoice_line.account: + break + if not invoice_line.account: + self.raise_user_error('missing_default_account_revenue', { + 'sale': self.sale.rec_name, + }) + invoice_line.stock_moves = [ + m for m in stock_moves if m.product == product] + lines.append(invoice_line) + return lines def _get_invoice_not_line(self): 'Return if the not line should be invoiced' diff --git a/setup.py b/setup.py index 03fd579d22..4fb4117019 100644 --- a/setup.py +++ b/setup.py @@ -126,5 +126,6 @@ setup(name=name, convert_2to3_doctests=[ 'tests/scenario_sale.rst', 'tests/scenario_sale_empty.rst', + 'tests/scenario_sale_ship_other_product.rst', ], ) diff --git a/tests/scenario_sale_ship_other_product.rst b/tests/scenario_sale_ship_other_product.rst new file mode 100644 index 0000000000..2ffc890e85 --- /dev/null +++ b/tests/scenario_sale_ship_other_product.rst @@ -0,0 +1,108 @@ +================================ +Sale Ship Other Product Scenario +================================ + +Imports:: + + >>> from decimal import Decimal + >>> from proteus import Model, Wizard + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import create_company, \ + ... get_company + >>> from trytond.modules.account.tests.tools import create_fiscalyear, \ + ... create_chart, get_accounts + +Install sale:: + + >>> config = activate_modules('sale') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + >>> revenue = accounts['revenue'] + +Create parties:: + + >>> Party = Model.get('party.party') + >>> customer = Party(name='Customer') + >>> customer.save() + +Create products:: + + >>> ProductUom = Model.get('product.uom') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> ProductTemplate = Model.get('product.template') + + >>> template = ProductTemplate() + >>> template.name = 'product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.salable = True + >>> template.list_price = Decimal('10') + >>> template.account_revenue = revenue + >>> product = template.products.new() + >>> template.save() + >>> product1, product2 = template.products + >>> product1.code = '1' + >>> product1.save() + >>> product2.code = '2' + >>> product2.save() + +Sale 5 products:: + + >>> Sale = Model.get('sale.sale') + >>> sale = Sale() + >>> sale.party = customer + >>> sale.invoice_method = 'shipment' + >>> line = sale.lines.new() + >>> line.product = product1 + >>> line.quantity = 5 + >>> sale.click('quote') + >>> sale.click('confirm') + >>> sale.click('process') + +Replace 2 products and ship:: + + >>> shipment, = sale.shipments + >>> shipment.click('draft') + >>> outgoing_move1, = shipment.outgoing_moves + >>> outgoing_move1.quantity = 3 + >>> outgoing_move1.save() + >>> outgoing_move2, = outgoing_move1.duplicate() + >>> outgoing_move2.quantity = 2 + >>> outgoing_move2.product = product2 + >>> outgoing_move2.save() + + >>> shipment.click('wait') + >>> shipment.click('assign_force') + >>> shipment.click('pack') + >>> shipment.click('done') + +Check invoice has 2 products:: + + >>> sale.reload() + >>> invoice, = sale.invoices + >>> len(invoice.lines) + 2 + >>> line1, = [l for l in invoice.lines if l.product == product1] + >>> line2, = [l for l in invoice.lines if l.product == product2] + + >>> line1.quantity + 3.0 + >>> line1.stock_moves == [outgoing_move1] + True + >>> line1.description + u'[1] product' + + >>> line2.quantity + 2.0 + >>> line2.stock_moves == [outgoing_move2] + True + >>> line2.description + u'[2] product' diff --git a/tests/test_sale.py b/tests/test_sale.py index f69fb705df..90f63ee7d0 100644 --- a/tests/test_sale.py +++ b/tests/test_sale.py @@ -23,4 +23,9 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, checker=doctest_checker)) + suite.addTests(doctest.DocFileSuite( + 'scenario_sale_ship_other_product.rst', + tearDown=doctest_teardown, encoding='utf-8', + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, + checker=doctest_checker)) return suite