← Back to team overview

openerp-community-reviewer team mailing list archive

lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe into lp:account-invoicing

 

Alexandre Fayolle - camptocamp has proposed merging lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe into lp:account-invoicing.

Requested reviews:
  Account Core Editors (account-core-editors)

For more details, see:
https://code.launchpad.net/~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe/+merge/199282

[ADD] sale_order_partial_invoice

This module allows invoicing a partial quantity of Sale Order lines

>From the module description:

    With a sale order in 'manual' invoicing policy, when the user selects to
    invoice some SO lines, a new wizard is display in which it is possible to
    select how much of the different lines is to be invoiced. The amounts
    invoiced and the amounts delivered are also displayed. When generating an
    invoice for the whole sale order, the partial invoices are taken into
    account and only the amounts not already invoiced are part of the new
    invoice
-- 
https://code.launchpad.net/~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe/+merge/199282
Your team Account Core Editors is requested to review the proposed merge of lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe into lp:account-invoicing.
=== added directory 'sale_order_partial_invoice'
=== added file 'sale_order_partial_invoice/__init__.py'
--- sale_order_partial_invoice/__init__.py	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/__init__.py	2013-12-17 13:14:35 +0000
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Alexandre Fayolle
+#    Copyright 2013 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import sale
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+

=== added file 'sale_order_partial_invoice/__openerp__.py'
--- sale_order_partial_invoice/__openerp__.py	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/__openerp__.py	2013-12-17 13:14:35 +0000
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Alexandre Fayolle
+#    Copyright 2013 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+    'name': 'Sale Partial Invoice',
+    'version': '1.1',
+    'category': 'Accounting & Finance',
+    'description': """Allow to partialy invoice Sale Order lines
+
+    With a sale order in 'manual' invoicing policy, when the user selects to
+    invoice some SO lines, a new wizard is display in which it is possible to
+    select how much of the different lines is to be invoiced. The amounts
+    invoiced and the amounts delivered are also displayed. When generating an
+    invoice for the whole sale order, the partial invoices are taken into
+    account and only the amounts not already invoiced are part of the new
+    invoice
+    """,
+    'author': 'Camptocamp',
+    'website': 'http://www.camptocamp.com',
+    'license': 'AGPL-3',
+    'depends': ['sale', 'account', 'sale_stock'],
+    'data': [
+        'sale_view.xml',
+    ],
+    'test': [
+        'test/partial_so_invoice.yml',
+        'test/make_remaining_invoice.yml',
+    ],
+    'demo': [],
+    'installable': True,
+    'active': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

=== added directory 'sale_order_partial_invoice/i18n'
=== added file 'sale_order_partial_invoice/invoice.py'
--- sale_order_partial_invoice/invoice.py	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/invoice.py	2013-12-17 13:14:35 +0000
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv import orm
+from openerp import netsvc
+from osv.orm import browse_record, browse_null
+
+class account_invoice(orm.Model):
+    _inherit = "account.invoice"
+
+    def _get_first_invoice_fields(self, cr, uid, invoice):
+        return {'origin': '%s' % (invoice.origin or '',),
+                'partner_id': invoice.partner_id.id,
+                'commercial_partner_id': invoice.commercial_partner_id.id,
+                'journal_id': invoice.journal_id.id,
+                'user_id': invoice.user_id.id,
+                'currency_id': invoice.currency_id.id,
+                'company_id': invoice.company_id.id,
+                'type': invoice.type,
+                'account_id': invoice.account_id.id,
+                'state': 'draft',
+                'reference': '%s' % (invoice.reference or '',),
+                'name': '%s' % (invoice.name or '',),
+                'fiscal_position': invoice.fiscal_position and invoice.fiscal_position.id or False,
+                'payment_term': invoice.payment_term and invoice.payment_term.id or False,
+                'period_id': invoice.period_id and invoice.period_id.id or False,
+                'invoice_line': [],
+                }
+
+    def _get_invoice_key_cols(self, cr, uid, invoice):
+        return ('partner_id', 'commercial_partner_id',
+                'user_id', 'type',
+                'account_id', 'currency_id',
+                'journal_id', 'company_id')
+
+    def _get_invoice_line_key_cols(self, cr, uid, invoice_line):
+        return ('name', 'origin', 'discount',
+                'invoice_line_tax_id', 'price_unit',
+                'product_id', 'account_id', 'quantity',
+                'account_analytic_id', 'sequence')
+
+    def do_merge(self, cr, uid, ids, context=None):
+        """
+        To merge similar type of account invoices.
+        Invoices will only be merged if:
+        * Account invoices are in draft
+        * Account invoices belong to the same partner
+        * Account invoices are have same company, partner, currency, journal, currency, salesman, account, type
+        Lines will only be merged if:
+        * Invoice lines are exactly the same except for the quantity and unit
+
+         @param self: The object pointer.
+         @param cr: A database cursor
+         @param uid: ID of the user currently logged in
+         @param ids: the ID or list of IDs
+         @param context: A standard dictionary
+
+         @return: new account invoice id
+
+        """
+        wf_service = netsvc.LocalService("workflow")
+        def make_key(br, fields):
+            list_key = []
+            for field in fields:
+                field_val = getattr(br, field)
+                if field in ('product_id', 'account_id'):
+                    if not field_val:
+                        field_val = False
+                if isinstance(field_val, browse_record):
+                    field_val = field_val.id
+                elif isinstance(field_val, browse_null):
+                    field_val = False
+                elif isinstance(field_val, list):
+                    field_val = ((6, 0, tuple([v.id for v in field_val])),)
+                list_key.append((field, field_val))
+            list_key.sort()
+            return tuple(list_key)
+
+        # compute what the new invoices should contain
+        new_invoices = {}
+        draft_invoices = [invoice
+                          for invoice in self.browse(cr, uid, ids, context=context)
+                          if invoice.state == 'draft']
+        seen_origins = {}
+        seen_client_refs = {}
+        for invoice in draft_invoices:
+            invoice_key = make_key(invoice, self._get_invoice_key_cols(cr, uid, invoice))
+            new_invoice = new_invoices.setdefault(invoice_key, ({}, []))
+            origins = seen_origins.setdefault(invoice_key, set())
+            client_refs = seen_client_refs.setdefault(invoice_key, set())
+            new_invoice[1].append(invoice.id)
+            invoice_infos = new_invoice[0]
+            if not invoice_infos:
+                invoice_infos.update(self._get_first_invoice_fields(cr, uid, invoice))
+                origins.add(invoice.origin)
+                client_refs.add(invoice.reference)
+            else:
+                if invoice.name:
+                    invoice_infos['name'] = (invoice_infos['name'] or '') + (' %s' % (invoice.name,))
+                if invoice.origin and invoice.origin not in origins:
+                    invoice_infos['origin'] = (invoice_infos['origin'] or '') + ' ' + invoice.origin
+                    origins.add(invoice.origin)
+                if invoice.reference and invoice.reference not in client_refs:
+                    invoice_infos['reference'] = (invoice_infos['reference'] or '') + (' %s' % (invoice.reference,))
+                    client_refs.add(invoice.reference)
+            for inv_line in invoice.invoice_line:
+                line_key = make_key(inv_line, self._get_invoice_line_key_cols(cr, uid, inv_line))
+                line_key = list(line_key)
+                if inv_line.uos_id:
+                    line_key.append(('uos_id', inv_line.uos_id.id))
+                invoice_infos['invoice_line'].append((0, 0, dict(line_key)))
+
+        allinvoices = []
+        invoices_info = {}
+        for invoice_key, (invoice_data, old_ids) in new_invoices.iteritems():
+            # skip merges with only one invoice
+            if len(old_ids) < 2:
+                allinvoices += (old_ids or [])
+                continue
+
+            # create the new invoice
+            newinvoice_id = self.create(cr, uid, invoice_data)
+            invoices_info.update({newinvoice_id: old_ids})
+            allinvoices.append(newinvoice_id)
+
+            # make triggers pointing to the old invoices point to the new invoice
+            for old_id in old_ids:
+                wf_service.trg_redirect(uid, 'account.invoice', old_id, newinvoice_id, cr)
+                wf_service.trg_validate(uid, 'account.invoice', old_id, 'invoice_cancel', cr)
+
+        return invoices_info
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+

=== added file 'sale_order_partial_invoice/sale.py'
--- sale_order_partial_invoice/sale.py	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/sale.py	2013-12-17 13:14:35 +0000
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    Author: Alexandre Fayolle
+#    Copyright 2013 Camptocamp SA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+ * Adding qty_invoiced field on SO lines, computed based on invoice lines
+   linked to it that has the same product. So this way, advance invoice will
+   still work !
+
+ * Adding qty_delivered field in SO Lines, computed from move lines linked to
+   it. For services, the quantity delivered is a problem, the MRP will
+   automatically run the procurement linked to this line and pass it to done. I
+   suggest that in that case, delivered qty = invoiced_qty as the procurement
+   is for the whole qty, it'll be a good alternative to follow what has been
+   done and not.
+
+ * Add in the "Order Line to invoice" view those fields
+
+ * Change the behavior of the "invoiced" field of the SO line to be true when
+   all is invoiced
+
+ * Adapt the "_make_invoice" method in SO to deal with qty_invoiced
+
+ * Adapt the sale_line_invoice.py wizard to deal with qty_invoiced, asking the
+   user how much he want to invoice.
+
+By having the delivered quantity, we can imagine in the future to provide an
+invoicing "based on delivery" that will look at those values instead of looking
+in picking.
+
+
+"""
+import pprint
+import logging
+_logger = logging.getLogger(__name__)
+
+from openerp.osv import orm, fields
+from openerp import netsvc
+from openerp.tools.translate import _
+
+
+class sale_order_line(orm.Model):
+    _inherit = 'sale.order.line'
+
+    def field_qty_invoiced(self, cr, uid, ids, fields, arg, context):
+        res = dict.fromkeys(ids, 0)
+        for line in self.browse(cr, uid, ids, context=context):
+            for invoice_line in line.invoice_lines:
+                if invoice_line.invoice_id.state != 'cancel':
+                    res[line.id] += invoice_line.quantity # XXX uom !
+        return res
+
+    def field_qty_delivered(self, cr, uid, ids, fields, arg, context):
+        res = dict.fromkeys(ids, 0)
+        for line in self.browse(cr, uid, ids, context=context):
+            if not line.move_ids:
+                # consumable or service: assume delivered == invoiced
+                res[line.id] = line.qty_invoiced
+            else:
+                for move in line.move_ids:
+                    if (move.state == 'done'
+                        and move.picking_id
+                        and move.picking_id.type == 'out'):
+                        res[line.id] += move.product_qty
+        return res
+
+    def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
+        res = super(sale_order_line,
+                    self)._prepare_order_line_invoice_line(cr,
+                                                           uid,
+                                                           line,
+                                                           account_id,
+                                                           context)
+        if '_partial_invoice' in context:
+            # we are making a partial invoice for the line
+            to_invoice_qty = context['_partial_invoice'][line.id]
+        else:
+            # we are invoicing the yet uninvoiced part of the line
+            to_invoice_qty = line.product_uom_qty - line.qty_invoiced
+        res['quantity'] = to_invoice_qty
+        return res
+
+    def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
+        res = dict.fromkeys(ids, False)
+        for this in self.browse(cr, uid, ids, context=context):
+            res[this.id] = (this.qty_invoiced == this.product_uom_qty)
+        return res
+
+    def _order_lines_from_invoice2(self, cr, uid, ids, context=None):
+        # overridden with different name because called by framework with
+        # 'self' an instance of another class
+        return self.pool['sale.order.line']._order_lines_from_invoice(cr, uid, ids, context)
+
+    _columns = {
+        'qty_invoiced': fields.function(field_qty_invoiced,
+                                        string='Invoiced Quantity',
+                                        type='float',
+                                        help="the quantity of product from this line "
+                                             "already invoiced"),
+        'qty_delivered': fields.function(field_qty_delivered,
+                                         string='Invoiced Quantity',
+                                         type='float',
+                                         help="the quantity of product from this line "
+                                              "already invoiced"),
+        'invoiced': fields.function(_fnct_line_invoiced,
+                                    string='Invoiced',
+                                    type='boolean',
+                                    store={
+                                        'account.invoice': (_order_lines_from_invoice2,
+                                                            ['state'], 10),
+                                        'sale.order.line': (
+                                            lambda self,cr,uid,ids,ctx=None: ids,
+                                            ['invoice_lines'], 10
+                                            )
+                                        }
+                                    ),
+        }
+
+
+class sale_order(orm.Model):
+    _inherit = 'sale.order'
+
+
+class sale_advance_payment_inv(orm.TransientModel):
+    _inherit = "sale.advance.payment.inv"
+
+    def create_invoices(self, cr, uid, ids, context=None):
+        """override standard behavior if payment method is set to 'lines':
+        """
+        import pprint
+        pprint.pprint(context)
+        res = super(sale_advance_payment_inv, self).create_invoices(cr, uid, ids, context)
+        pprint.pprint(context)
+        wizard = self.browse(cr, uid, ids[0], context)
+        if wizard.advance_payment_method != 'lines':
+            return res
+        sale_ids = context.get('active_ids', [])
+        if not sale_ids:
+            return res
+        wizard_obj = self.pool['sale.order.line.invoice.partially']
+        order_line_obj = self.pool['sale.order.line']
+        so_domain = [('order_id', 'in', sale_ids),]
+        so_line_ids = order_line_obj.search(cr, uid, so_domain, context=context)
+        line_values = []
+        for so_line in order_line_obj.browse(cr, uid, so_line_ids, context=context):
+            if so_line.state in ('confirmed', 'done') and not so_line.invoiced:
+                val = {'sale_order_line_id': so_line.id,}
+                if so_line.product_id and so_line.product_id.type == 'product':
+                    val['quantity'] = so_line.qty_delivered - so_line.qty_invoiced
+                else:
+                    # service or consumable
+                    val['quantity'] = so_line.product_uom_qty - so_line.qty_invoiced
+                line_values.append((0, 0, val))
+        val = {'line_ids': line_values,}
+        pprint.pprint(val)
+        wizard_id = wizard_obj.create(cr, uid, val, context=context)
+        wiz = wizard_obj.browse(cr, uid, wizard_id, context=context)
+        print wiz.line_ids
+        res = {'view_type': 'form',
+               'view_mode': 'form',
+               'res_model': 'sale.order.line.invoice.partially',
+               'res_id': wizard_id,
+               'type': 'ir.actions.act_window',
+               'target': 'new',
+               'context': context,
+               }
+        return res
+
+class sale_order_line_invoice_partially_line(orm.TransientModel):
+    _name = "sale.order.line.invoice.partially.line"
+    _columns = {
+        'wizard_id': fields.many2one('sale.order.line.invoice.partially',
+                                     string='Wizard'),
+        'sale_order_line_id': fields.many2one('sale.order.line',
+                                              string='sale.order.line'),
+        'name': fields.related('sale_order_line_id', 'name',
+                               type='char', string="Line"),
+        'order_qty': fields.related('sale_order_line_id', 'product_uom_qty',
+                                    type='float', string="Sold"),
+        'qty_invoiced': fields.related('sale_order_line_id', 'qty_invoiced',
+                                       type='float', string="Invoiced"),
+        'qty_delivered': fields.related('sale_order_line_id', 'qty_delivered',
+                                        type='float', string="Shipped"),
+        'quantity': fields.float('To invoice'),
+        }
+
+
+class sale_order_line_invoice_partially(orm.TransientModel):
+    _name = "sale.order.line.invoice.partially"
+    _columns = {
+        'name': fields.char('Name'),
+        'line_ids': fields.one2many('sale.order.line.invoice.partially.line',
+                                    'wizard_id', string="Lines"),
+        }
+
+    def create_invoice(self, cr, uid, ids, context=None):
+        wf_service = netsvc.LocalService('workflow')
+        if context is None:
+            context = {}
+        ctx = context.copy()
+        ctx['_partial_invoice'] = {}
+        so_line_obj = self.pool['sale.order.line']
+        so_obj = self.pool['sale.order']
+        order_lines = {}
+        for wiz in self.browse(cr, uid, ids, context=context):
+            for line in wiz.line_ids:
+                if line.quantity == 0:
+                    continue
+                sale_order = line.sale_order_line_id.order_id
+                if sale_order.id not in order_lines:
+                    order_lines[sale_order.id] = []
+                order_lines[sale_order.id].append(line.sale_order_line_id.id)
+                ctx['_partial_invoice'][line.sale_order_line_id.id] = line.quantity
+        for order_id in order_lines:
+            line_ids = order_lines[order_id]
+            pprint.pprint(line_ids)
+            invoice_line_ids = so_line_obj.invoice_line_create(cr,
+                                                               uid,
+                                                               line_ids,
+                                                               context=ctx)
+            order = so_obj.browse(cr, uid, order_id, context=context)
+            invoice_id = so_obj._make_invoice(cr, uid,
+                                              order,
+                                              invoice_line_ids,
+                                              context=ctx)
+            _logger.info('created invoice %d', invoice_id)
+            # the following is copied from many places around
+            # (actually sale_line_invoice.py)
+            cr.execute('INSERT INTO sale_order_invoice_rel (order_id, '
+                       '                                    invoice_id) '
+                       'VALUES (%s,%s)', (order_id, invoice_id))
+            if all(line.invoiced for line in order.order_line):
+                wf_service.trg_validate(uid, 'sale.order', order.id, 'manual_invoice', cr)
+        return {'type': 'ir.actions.act_window_close'}

=== added file 'sale_order_partial_invoice/sale_view.xml'
--- sale_order_partial_invoice/sale_view.xml	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/sale_view.xml	2013-12-17 13:14:35 +0000
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+  <data>
+
+    <record id="view_order_line_tree" model="ir.ui.view">
+      <field name="name">sale.order.line.tree</field>
+      <field name="model">sale.order.line</field>
+      <field name="inherit_id" ref="sale.view_order_line_tree"/>
+      <field name="arch" type="xml">
+        <field name="product_uom_qty" position="after">
+          <field name="qty_invoiced" string="Inv. Qty"/>
+          <field name="qty_delivered" string="Shipped Qty"/>
+        </field>
+      </field>
+    </record>
+
+    <record id="view_order_line_form2" model="ir.ui.view">
+      <field name="name">sale.order.line.form2</field>
+      <field name="model">sale.order.line</field>
+      <field name="inherit_id" ref="sale.view_order_line_form2"/>
+      <field name="arch" type="xml">
+        <xpath expr="//field[@name='product_uom_qty']/.." position="after">
+          <field name="qty_invoiced" />
+          <field name="qty_delivered"/>
+        </xpath>
+      </field>
+    </record>
+
+    <record id="view_sale_order_invoice_partially_form" model="ir.ui.view">
+      <field name="name">sale.order.line.invoice.partially.form</field>
+      <field name="model">sale.order.line.invoice.partially</field>
+      <field name="arch" type="xml">
+        <form version="7.0" string="Invoice Sale Order Lines">
+        <sheet>
+          <separator string="Invoice lines"/>
+          <group>
+            <field name="line_ids" nolabel="1">
+              <tree version="7.0" string="Lines" create="false" editable="bottom">
+                <field name="sale_order_line_id" invisible="1"/>
+                <field name="name"/>
+                <field name="order_qty"/>
+                <field name="qty_invoiced"/>
+                <field name="qty_delivered"/>
+                <field name="quantity"/>
+              </tree>
+            </field>
+          </group>
+        </sheet>
+        <footer>
+          <button name="create_invoice" type="object" string="Create Invoice" class="oe_highlight"/>
+            or
+            <button string="Cancel" class="oe_link" special="cancel" />
+        </footer>
+        </form>
+      </field>
+    </record>
+
+  </data>
+</openerp>

=== added directory 'sale_order_partial_invoice/test'
=== added file 'sale_order_partial_invoice/test/make_remaining_invoice.yml'
--- sale_order_partial_invoice/test/make_remaining_invoice.yml	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/test/make_remaining_invoice.yml	2013-12-17 13:14:35 +0000
@@ -0,0 +1,169 @@
+-
+ Create stockable products for testing purpose
+-
+  !record {model: product.product, id: product_stockable}:
+    name: 'LCD Projector'
+    list_price: 1234.99
+    type: product
+    procure_method: 'make_to_stock'
+-
+  !record {model: product.product, id: product_service}:
+    name: 'LCD Projector 3 Year Warranty'
+    list_price: 299.99
+    type: service
+    procure_method: 'make_to_stock'
+-
+  !record {model: product.product, id: product_consumable}:
+    name: 'Spare screws'
+    list_price: 1.22
+    type: consu
+    procure_method: 'make_to_stock'
+-
+ Create stock for the stockable product
+- 
+  !record {model: stock.move, id: move1}:
+    location_id: stock.stock_location_suppliers
+    location_dest_id: stock.stock_location_stock
+    product_id: product_stockable
+    product_qty: 5
+    state: done
+-
+ Create a sale order for the above
+-
+  !record {model: sale.order, id: sale_order2}:
+    partner_id: base.res_partner_2
+    order_policy: manual
+    order_line:
+      - product_id: product_stockable
+        product_uom_qty: 3
+      - product_id: product_service
+        product_uom_qty: 3
+      - product_id: product_consumable
+        product_uom_qty: 12
+-
+ Confirm Order
+-
+  !workflow {model: sale.order, action: order_confirm, ref: sale_order2}
+-
+ Partial delivery
+-
+  !python {model: stock.picking}: |
+    delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2"))])
+    first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+    if first_picking.force_assign(cr, uid, first_picking):
+      assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
+      partial_moves = {}
+      for move in first_picking.move_lines:
+        partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
+      first_picking.do_partial(partial_moves, context=context)
+-
+ Create partial invoice
+-
+   !python {model: sale.order.line.invoice.partially}: |
+    sale_obj = self.pool['sale.order']
+    sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order2"), context=context)
+    wiz_data = {'name': 'wiz1',
+                'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
+                             (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
+                             (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
+                            ],
+                }
+    id = self.create(cr, uid, wiz_data, context=context)
+    self.create_invoice(cr, uid, [id], context=context)
+-
+ Check the 1st invoice is created
+-
+  !python {model: sale.order}: |
+    order = self.browse(cr, uid, ref('sale_order2'))
+    assert len(order.invoice_ids) == 1, "I should have 1 invoice"
+-
+ Check qty_invoiced 4
+-
+  !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 1
+    - order_line[1].qty_invoiced == 1
+    - order_line[2].qty_invoiced == 4
+-
+ Check qty_delivered 4
+-
+  !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 1
+    - order_line[1].qty_delivered == 1
+    - order_line[2].qty_delivered == 4
+-
+ Partial delivery 2
+-
+  !python {model: stock.picking}: |
+    delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2")), ('state', '!=', 'done')])
+    picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+    if picking.force_assign(cr, uid, picking):
+      assert len(picking.move_lines) == 2, "wrong count of stock moves"
+      partial_moves = {}
+      for move in picking.move_lines:
+        partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
+      picking.do_partial(partial_moves, context=context)
+-
+  I invoice the remaining of the SO
+-
+  !python {model: sale.advance.payment.inv}: |
+    ctx = context.copy()
+    ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order2")], "active_id":ref("sale_order2")})
+    pay_id = self.create(cr, uid, {'advance_payment_method': 'all'})
+    self.create_invoices(cr, uid, [pay_id], context=ctx)
+-
+ Check the invoiced qtys in the invoice
+-
+  !python {model: sale.order}: |
+    order = self.browse(cr, uid, ref('sale_order2'))
+    assert len(order.invoice_ids) == 2, "I should have 2 invoices"
+-
+ Check qty_invoiced 5
+-
+  !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 3
+    - order_line[1].qty_invoiced == 3
+    - order_line[2].qty_invoiced == 12
+-
+ Check qty_delivered 5
+-
+  !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 3
+    - order_line[1].qty_delivered == 3
+    - order_line[2].qty_delivered == 12
+-
+  I confirm the invoices
+-
+  !python {model: sale.order}: |
+    import netsvc
+    wf_service = netsvc.LocalService("workflow")
+    so = self.browse(cr, uid, ref("sale_order2"))
+    for invoice in so.invoice_ids:
+      wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
+-
+  I pay the invoices.
+-
+  !python {model: account.invoice}: |
+    sale_order = self.pool.get('sale.order')
+    order = sale_order.browse(cr, uid, ref("sale_order2"))
+    journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
+    for invoice in order.invoice_ids:
+        invoice.pay_and_reconcile(
+            invoice.amount_total, ref('account.cash'), ref('account.period_8'),
+            journal_ids[0], ref('account.cash'),
+            ref('account.period_8'), journal_ids[0],
+            name='test')
+-
+  I run the scheduler (required to get the correct sale order state)
+-
+  !python {model: procurement.order}: |
+    self.run_scheduler(cr, uid)
+-
+  I check sale order
+-
+  !python {model: sale.order}: |
+    sale_order = self.browse(cr, uid, ref("sale_order2"))
+    assert sale_order.invoice_ids, "Invoice should be created."
+    assert sale_order.invoice_exists, "Order is not invoiced."
+    assert sale_order.invoiced, "Order is not paid."
+    assert sale_order.state == 'done', 'Order should be Done.'
+

=== added file 'sale_order_partial_invoice/test/partial_so_invoice.yml'
--- sale_order_partial_invoice/test/partial_so_invoice.yml	1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/test/partial_so_invoice.yml	2013-12-17 13:14:35 +0000
@@ -0,0 +1,231 @@
+-
+ Create stockable products for testing purpose
+-
+  !record {model: product.product, id: product_stockable}:
+    name: 'LCD Projector'
+    list_price: 1234.99
+    type: product
+    procure_method: 'make_to_stock'
+-
+  !record {model: product.product, id: product_service}:
+    name: 'LCD Projector 3 Year Warranty'
+    list_price: 299.99
+    type: service
+    procure_method: 'make_to_stock'
+-
+  !record {model: product.product, id: product_consumable}:
+    name: 'Spare screws'
+    list_price: 1.22
+    type: consu
+    procure_method: 'make_to_stock'
+-
+ Create stock for the stockable product
+- 
+  !record {model: stock.move, id: move1}:
+    location_id: stock.stock_location_suppliers
+    location_dest_id: stock.stock_location_stock
+    product_id: product_stockable
+    product_qty: 5
+    state: done
+-
+ Create a sale order for the above
+-
+  !record {model: sale.order, id: sale_order1}:
+    partner_id: base.res_partner_2
+    order_policy: manual
+    order_line:
+      - product_id: product_stockable
+        product_uom_qty: 3
+      - product_id: product_service
+        product_uom_qty: 3
+      - product_id: product_consumable
+        product_uom_qty: 12
+-
+ Check qty_invoiced 1
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 0
+    - order_line[1].qty_invoiced == 0
+    - order_line[2].qty_invoiced == 0
+-
+ Check qty_delivered 1
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 0
+    - order_line[1].qty_delivered == 0
+    - order_line[2].qty_delivered == 0
+-
+ Confirm Order
+-
+  !workflow {model: sale.order, action: order_confirm, ref: sale_order1}
+-
+ Check qty_invoiced 2
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 0
+    - order_line[1].qty_invoiced == 0
+    - order_line[2].qty_invoiced == 0
+-
+ Check qty_delivered 2
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 0
+    - order_line[1].qty_delivered == 0
+    - order_line[2].qty_delivered == 0
+-
+ Partial delivery
+-
+  !python {model: stock.picking}: |
+    delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1"))])
+    first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+    if first_picking.force_assign(cr, uid, first_picking):
+      assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
+      partial_moves = {}
+      for move in first_picking.move_lines:
+        partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
+      first_picking.do_partial(partial_moves, context=context)
+-
+ Check qty_invoiced 3
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 0
+    - order_line[1].qty_invoiced == 0
+    - order_line[2].qty_invoiced == 0
+-
+ Check qty_delivered 3
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 1
+    - order_line[1].qty_delivered == 0
+    - order_line[2].qty_delivered == 4
+-
+  Check SO state
+-
+  !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
+     - state == 'manual'
+-
+ Create partial invoice
+-
+   !python {model: sale.order.line.invoice.partially}: |
+    sale_obj = self.pool['sale.order']
+    sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order1"), context=context)
+    wiz_data = {'name': 'wiz1',
+                'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
+                             (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
+                             (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
+                            ],
+                }
+    id = self.create(cr, uid, wiz_data, context=context)
+    self.create_invoice(cr, uid, [id], context=context)
+-
+ Check the 1st invoice is created
+-
+  !python {model: sale.order}: |
+    order = self.browse(cr, uid, ref('sale_order1'))
+    assert len(order.invoice_ids) == 1, "I should have 1 invoice"
+-
+ Check qty_invoiced 4
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 1
+    - order_line[1].qty_invoiced == 1
+    - order_line[2].qty_invoiced == 4
+-
+ Check qty_delivered 4
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 1
+    - order_line[1].qty_delivered == 1
+    - order_line[2].qty_delivered == 4
+-
+  Check SO state
+-
+  !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
+     - state == 'manual'
+-
+ Partial delivery 2
+-
+  !python {model: stock.picking}: |
+    delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1")), ('state', '!=', 'done')])
+    picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+    if picking.force_assign(cr, uid, picking):
+      assert len(picking.move_lines) == 2, "wrong count of stock moves"
+      partial_moves = {}
+      for move in picking.move_lines:
+        partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
+      picking.do_partial(partial_moves, context=context)
+-
+ Create partial invoice 2
+-
+   !python {model: sale.order.line.invoice.partially}: |
+    sale_obj = self.pool['sale.order']
+    sale = sale_obj.browse(cr, uid, ref("sale_order1"), context=context)
+    wiz_data = {'name': 'wiz2',
+                'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 2}),
+                             (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 2}),
+                             (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 8}),
+                            ],
+                }
+    id = self.create(cr, uid, wiz_data, context=context)
+    self.create_invoice(cr, uid, [id], context=context)
+-
+ Check the 2nd invoice is created
+-
+  !python {model: sale.order}: |
+    order = self.browse(cr, uid, ref('sale_order1'))
+    assert len(order.invoice_ids) == 2, "I should have 2 invoices"
+-
+ Check qty_invoiced 5
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+    - order_line[0].qty_invoiced == 3
+    - order_line[1].qty_invoiced == 3
+    - order_line[2].qty_invoiced == 12
+-
+ Check qty_delivered 5
+-
+  !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+    - order_line[0].qty_delivered == 3
+    - order_line[1].qty_delivered == 3
+    - order_line[2].qty_delivered == 12
+-
+  Check SO state
+-
+  !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
+     - state == 'manual'
+-
+  I confirm the invoices
+-
+  !python {model: sale.order}: |
+    import netsvc
+    wf_service = netsvc.LocalService("workflow")
+    so = self.browse(cr, uid, ref("sale_order1"))
+    for invoice in so.invoice_ids:
+      wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
+-
+  I pay the invoices.
+-
+  !python {model: account.invoice}: |
+    sale_order = self.pool.get('sale.order')
+    order = sale_order.browse(cr, uid, ref("sale_order1"))
+    journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
+    for invoice in order.invoice_ids:
+        invoice.pay_and_reconcile(
+            invoice.amount_total, ref('account.cash'), ref('account.period_8'),
+            journal_ids[0], ref('account.cash'),
+            ref('account.period_8'), journal_ids[0],
+            name='test')
+-
+  I run the scheduler (required to get the correct sale order state)
+-
+  !python {model: procurement.order}: |
+    self.run_scheduler(cr, uid)
+-
+  I check sale order
+-
+  !python {model: sale.order}: |
+    sale_order = self.browse(cr, uid, ref("sale_order1"))
+    assert sale_order.invoice_ids, "Invoice should be created."
+    assert sale_order.invoice_exists, "Order is not invoiced."
+    assert sale_order.invoiced, "Order is not paid."
+    assert sale_order.state == 'done', 'Order should be Done.'


Follow ups