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

Proposed by Alexandre Fayolle - camptocamp
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 30
Merged at revision: 26
Proposed branch: lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe
Merge into: lp:~account-core-editors/account-invoicing/7.0
Diff against target: 805 lines (+773/-0)
6 files modified
sale_order_partial_invoice/__init__.py (+23/-0)
sale_order_partial_invoice/__openerp__.py (+51/-0)
sale_order_partial_invoice/sale.py (+240/-0)
sale_order_partial_invoice/sale_view.xml (+59/-0)
sale_order_partial_invoice/test/make_remaining_invoice.yml (+169/-0)
sale_order_partial_invoice/test/partial_so_invoice.yml (+231/-0)
To merge this branch: bzr merge lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe
Reviewer Review Type Date Requested Status
Joël Grand-Guillaume @ camptocamp code review + tests Needs Information
Nicolas Bessi - Camptocamp (community) no test, code review Approve
Leonardo Pistone code review Approve
Review via email: mp+199282@code.launchpad.net

Description of the change

[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

To post a comment you must log in.
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

To use it / test it:

* create a SO with several lines (products and services can be used),
with "manual" (On Demand) invoicing policy
* deliver (partially or completely)
* click "Create Invoice" and select "Invoice some order lines"

A wizard is displayed instead of the List view of sale order line, and
it is possible in that wizard to change the quantity which will appear
in the invoice. The already invoiced and delivered quantities are also
displayed (for services the delivered quantity is equal to the invoiced
quantity).

If a partial invoice was generated, generating a new invoice for the
whole Sale Order takes the partial invoice into account.

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Hello,

invoice.py is never imported.
Confirmed with Alexandre, dead code I will fix it.
Put MP in workin progress unitl fix commited.

Regards

Nicolas

Revision history for this message
Leonardo Pistone (lepistone) wrote :

Thanks for your contribution Alexandre!

* some pprints around
* a header mentions Tiny

review: Needs Fixing
27. By Nicolas Bessi - Camptocamp

[FIX] forgotten pprint

28. By Nicolas Bessi - Camptocamp

[FIX] unused import of _

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

class sale_order(orm.Model):
    _inherit = 'sale.order'

Dead code ??

review: Needs Information
Revision history for this message
Leonardo Pistone (lepistone) wrote :

Since Nicolas has fixed what I pointed out, I change my review.

Thanks!

review: Approve (code review)
29. By Nicolas Bessi - Camptocamp

[FIX] PEP8 condition operator at end (personal taste)

30. By Nicolas Bessi - Camptocamp

[FIX] unused initialized class

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Just committed small neat pick.

LGTM, but it would be nice if Alexandre can just redo a run of test with last fixes.

Regards

Nicolas

review: Approve (no test, code review)
Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi Alexandre,

This contrib, really rocks ! A question:

 * When displaying the lines and quantities to invoice (with Line;Sold;Ivoiced;Shipped;To invoice ), I didn't understand why fields are not in read-only except "To invoice" one ? Any reason not to let the user change other field ?

A part from that, it looked perfect to me, it was just like this that I expected this to work, thanks.

Regards,

Joël

review: Needs Information (code review + tests)
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

> Hi Alexandre,
>
>
> This contrib, really rocks ! A question:
>
> * When displaying the lines and quantities to invoice (with
> Line;Sold;Ivoiced;Shipped;To invoice ), I didn't understand why fields are not
> in read-only except "To invoice" one ? Any reason not to let the user change
> other field ?

You are right, I'll make the required changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'sale_order_partial_invoice'
2=== added file 'sale_order_partial_invoice/__init__.py'
3--- sale_order_partial_invoice/__init__.py 1970-01-01 00:00:00 +0000
4+++ sale_order_partial_invoice/__init__.py 2013-12-17 14:13:31 +0000
5@@ -0,0 +1,23 @@
6+# -*- coding: utf-8 -*-
7+##############################################################################
8+#
9+# Author: Alexandre Fayolle
10+# Copyright 2013 Camptocamp SA
11+#
12+# This program is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Affero General Public License as
14+# published by the Free Software Foundation, either version 3 of the
15+# License, or (at your option) any later version.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU Affero General Public License for more details.
21+#
22+# You should have received a copy of the GNU Affero General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+#
25+##############################################################################
26+from . import sale
27+
28+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
29
30=== added file 'sale_order_partial_invoice/__openerp__.py'
31--- sale_order_partial_invoice/__openerp__.py 1970-01-01 00:00:00 +0000
32+++ sale_order_partial_invoice/__openerp__.py 2013-12-17 14:13:31 +0000
33@@ -0,0 +1,51 @@
34+# -*- coding: utf-8 -*-
35+##############################################################################
36+#
37+# Author: Alexandre Fayolle
38+# Copyright 2013 Camptocamp SA
39+#
40+# This program is free software: you can redistribute it and/or modify
41+# it under the terms of the GNU Affero General Public License as
42+# published by the Free Software Foundation, either version 3 of the
43+# License, or (at your option) any later version.
44+#
45+# This program is distributed in the hope that it will be useful,
46+# but WITHOUT ANY WARRANTY; without even the implied warranty of
47+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
48+# GNU Affero General Public License for more details.
49+#
50+# You should have received a copy of the GNU Affero General Public License
51+# along with this program. If not, see <http://www.gnu.org/licenses/>.
52+#
53+##############################################################################
54+
55+{
56+ 'name': 'Sale Partial Invoice',
57+ 'version': '1.1',
58+ 'category': 'Accounting & Finance',
59+ 'description': """Allow to partialy invoice Sale Order lines
60+
61+ With a sale order in 'manual' invoicing policy, when the user selects to
62+ invoice some SO lines, a new wizard is display in which it is possible to
63+ select how much of the different lines is to be invoiced. The amounts
64+ invoiced and the amounts delivered are also displayed. When generating an
65+ invoice for the whole sale order, the partial invoices are taken into
66+ account and only the amounts not already invoiced are part of the new
67+ invoice
68+ """,
69+ 'author': 'Camptocamp',
70+ 'website': 'http://www.camptocamp.com',
71+ 'license': 'AGPL-3',
72+ 'depends': ['sale', 'account', 'sale_stock'],
73+ 'data': [
74+ 'sale_view.xml',
75+ ],
76+ 'test': [
77+ 'test/partial_so_invoice.yml',
78+ 'test/make_remaining_invoice.yml',
79+ ],
80+ 'demo': [],
81+ 'installable': True,
82+ 'active': False,
83+}
84+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
85
86=== added directory 'sale_order_partial_invoice/i18n'
87=== added file 'sale_order_partial_invoice/sale.py'
88--- sale_order_partial_invoice/sale.py 1970-01-01 00:00:00 +0000
89+++ sale_order_partial_invoice/sale.py 2013-12-17 14:13:31 +0000
90@@ -0,0 +1,240 @@
91+# -*- coding: utf-8 -*-
92+##############################################################################
93+#
94+# Author: Alexandre Fayolle
95+# Copyright 2013 Camptocamp SA
96+#
97+# This program is free software: you can redistribute it and/or modify
98+# it under the terms of the GNU Affero General Public License as
99+# published by the Free Software Foundation, either version 3 of the
100+# License, or (at your option) any later version.
101+#
102+# This program is distributed in the hope that it will be useful,
103+# but WITHOUT ANY WARRANTY; without even the implied warranty of
104+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
105+# GNU Affero General Public License for more details.
106+#
107+# You should have received a copy of the GNU Affero General Public License
108+# along with this program. If not, see <http://www.gnu.org/licenses/>.
109+#
110+##############################################################################
111+"""
112+ * Adding qty_invoiced field on SO lines, computed based on invoice lines
113+ linked to it that has the same product. So this way, advance invoice will
114+ still work !
115+
116+ * Adding qty_delivered field in SO Lines, computed from move lines linked to
117+ it. For services, the quantity delivered is a problem, the MRP will
118+ automatically run the procurement linked to this line and pass it to done. I
119+ suggest that in that case, delivered qty = invoiced_qty as the procurement
120+ is for the whole qty, it'll be a good alternative to follow what has been
121+ done and not.
122+
123+ * Add in the "Order Line to invoice" view those fields
124+
125+ * Change the behavior of the "invoiced" field of the SO line to be true when
126+ all is invoiced
127+
128+ * Adapt the "_make_invoice" method in SO to deal with qty_invoiced
129+
130+ * Adapt the sale_line_invoice.py wizard to deal with qty_invoiced, asking the
131+ user how much he want to invoice.
132+
133+By having the delivered quantity, we can imagine in the future to provide an
134+invoicing "based on delivery" that will look at those values instead of looking
135+in picking.
136+
137+
138+"""
139+import logging
140+_logger = logging.getLogger(__name__)
141+
142+from openerp.osv import orm, fields
143+from openerp import netsvc
144+
145+
146+class sale_order_line(orm.Model):
147+ _inherit = 'sale.order.line'
148+
149+ def field_qty_invoiced(self, cr, uid, ids, fields, arg, context):
150+ res = dict.fromkeys(ids, 0)
151+ for line in self.browse(cr, uid, ids, context=context):
152+ for invoice_line in line.invoice_lines:
153+ if invoice_line.invoice_id.state != 'cancel':
154+ res[line.id] += invoice_line.quantity # XXX uom !
155+ return res
156+
157+ def field_qty_delivered(self, cr, uid, ids, fields, arg, context):
158+ res = dict.fromkeys(ids, 0)
159+ for line in self.browse(cr, uid, ids, context=context):
160+ if not line.move_ids:
161+ # consumable or service: assume delivered == invoiced
162+ res[line.id] = line.qty_invoiced
163+ else:
164+ for move in line.move_ids:
165+ if (move.state == 'done' and
166+ move.picking_id and
167+ move.picking_id.type == 'out'):
168+ res[line.id] += move.product_qty
169+ return res
170+
171+ def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
172+ res = super(sale_order_line,
173+ self)._prepare_order_line_invoice_line(cr,
174+ uid,
175+ line,
176+ account_id,
177+ context)
178+ if '_partial_invoice' in context:
179+ # we are making a partial invoice for the line
180+ to_invoice_qty = context['_partial_invoice'][line.id]
181+ else:
182+ # we are invoicing the yet uninvoiced part of the line
183+ to_invoice_qty = line.product_uom_qty - line.qty_invoiced
184+ res['quantity'] = to_invoice_qty
185+ return res
186+
187+ def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
188+ res = dict.fromkeys(ids, False)
189+ for this in self.browse(cr, uid, ids, context=context):
190+ res[this.id] = (this.qty_invoiced == this.product_uom_qty)
191+ return res
192+
193+ def _order_lines_from_invoice2(self, cr, uid, ids, context=None):
194+ # overridden with different name because called by framework with
195+ # 'self' an instance of another class
196+ return self.pool['sale.order.line']._order_lines_from_invoice(cr, uid, ids, context)
197+
198+ _columns = {
199+ 'qty_invoiced': fields.function(field_qty_invoiced,
200+ string='Invoiced Quantity',
201+ type='float',
202+ help="the quantity of product from this line "
203+ "already invoiced"),
204+ 'qty_delivered': fields.function(field_qty_delivered,
205+ string='Invoiced Quantity',
206+ type='float',
207+ help="the quantity of product from this line "
208+ "already invoiced"),
209+ 'invoiced': fields.function(_fnct_line_invoiced,
210+ string='Invoiced',
211+ type='boolean',
212+ store={
213+ 'account.invoice': (_order_lines_from_invoice2,
214+ ['state'], 10),
215+ 'sale.order.line': (
216+ lambda self,cr,uid,ids,ctx=None: ids,
217+ ['invoice_lines'], 10
218+ )
219+ }
220+ ),
221+ }
222+
223+
224+class sale_advance_payment_inv(orm.TransientModel):
225+ _inherit = "sale.advance.payment.inv"
226+
227+ def create_invoices(self, cr, uid, ids, context=None):
228+ """override standard behavior if payment method is set to 'lines':
229+ """
230+ res = super(sale_advance_payment_inv, self).create_invoices(cr, uid, ids, context)
231+ wizard = self.browse(cr, uid, ids[0], context)
232+ if wizard.advance_payment_method != 'lines':
233+ return res
234+ sale_ids = context.get('active_ids', [])
235+ if not sale_ids:
236+ return res
237+ wizard_obj = self.pool['sale.order.line.invoice.partially']
238+ order_line_obj = self.pool['sale.order.line']
239+ so_domain = [('order_id', 'in', sale_ids),]
240+ so_line_ids = order_line_obj.search(cr, uid, so_domain, context=context)
241+ line_values = []
242+ for so_line in order_line_obj.browse(cr, uid, so_line_ids, context=context):
243+ if so_line.state in ('confirmed', 'done') and not so_line.invoiced:
244+ val = {'sale_order_line_id': so_line.id,}
245+ if so_line.product_id and so_line.product_id.type == 'product':
246+ val['quantity'] = so_line.qty_delivered - so_line.qty_invoiced
247+ else:
248+ # service or consumable
249+ val['quantity'] = so_line.product_uom_qty - so_line.qty_invoiced
250+ line_values.append((0, 0, val))
251+ val = {'line_ids': line_values,}
252+ wizard_id = wizard_obj.create(cr, uid, val, context=context)
253+ wiz = wizard_obj.browse(cr, uid, wizard_id, context=context)
254+ print wiz.line_ids
255+ res = {'view_type': 'form',
256+ 'view_mode': 'form',
257+ 'res_model': 'sale.order.line.invoice.partially',
258+ 'res_id': wizard_id,
259+ 'type': 'ir.actions.act_window',
260+ 'target': 'new',
261+ 'context': context,
262+ }
263+ return res
264+
265+
266+class sale_order_line_invoice_partially_line(orm.TransientModel):
267+ _name = "sale.order.line.invoice.partially.line"
268+ _columns = {
269+ 'wizard_id': fields.many2one('sale.order.line.invoice.partially',
270+ string='Wizard'),
271+ 'sale_order_line_id': fields.many2one('sale.order.line',
272+ string='sale.order.line'),
273+ 'name': fields.related('sale_order_line_id', 'name',
274+ type='char', string="Line"),
275+ 'order_qty': fields.related('sale_order_line_id', 'product_uom_qty',
276+ type='float', string="Sold"),
277+ 'qty_invoiced': fields.related('sale_order_line_id', 'qty_invoiced',
278+ type='float', string="Invoiced"),
279+ 'qty_delivered': fields.related('sale_order_line_id', 'qty_delivered',
280+ type='float', string="Shipped"),
281+ 'quantity': fields.float('To invoice'),
282+ }
283+
284+
285+class sale_order_line_invoice_partially(orm.TransientModel):
286+ _name = "sale.order.line.invoice.partially"
287+ _columns = {
288+ 'name': fields.char('Name'),
289+ 'line_ids': fields.one2many('sale.order.line.invoice.partially.line',
290+ 'wizard_id', string="Lines"),
291+ }
292+
293+ def create_invoice(self, cr, uid, ids, context=None):
294+ wf_service = netsvc.LocalService('workflow')
295+ if context is None:
296+ context = {}
297+ ctx = context.copy()
298+ ctx['_partial_invoice'] = {}
299+ so_line_obj = self.pool['sale.order.line']
300+ so_obj = self.pool['sale.order']
301+ order_lines = {}
302+ for wiz in self.browse(cr, uid, ids, context=context):
303+ for line in wiz.line_ids:
304+ if line.quantity == 0:
305+ continue
306+ sale_order = line.sale_order_line_id.order_id
307+ if sale_order.id not in order_lines:
308+ order_lines[sale_order.id] = []
309+ order_lines[sale_order.id].append(line.sale_order_line_id.id)
310+ ctx['_partial_invoice'][line.sale_order_line_id.id] = line.quantity
311+ for order_id in order_lines:
312+ line_ids = order_lines[order_id]
313+ invoice_line_ids = so_line_obj.invoice_line_create(cr,
314+ uid,
315+ line_ids,
316+ context=ctx)
317+ order = so_obj.browse(cr, uid, order_id, context=context)
318+ invoice_id = so_obj._make_invoice(cr, uid,
319+ order,
320+ invoice_line_ids,
321+ context=ctx)
322+ _logger.info('created invoice %d', invoice_id)
323+ # the following is copied from many places around
324+ # (actually sale_line_invoice.py)
325+ cr.execute('INSERT INTO sale_order_invoice_rel (order_id, '
326+ ' invoice_id) '
327+ 'VALUES (%s,%s)', (order_id, invoice_id))
328+ if all(line.invoiced for line in order.order_line):
329+ wf_service.trg_validate(uid, 'sale.order', order.id, 'manual_invoice', cr)
330+ return {'type': 'ir.actions.act_window_close'}
331
332=== added file 'sale_order_partial_invoice/sale_view.xml'
333--- sale_order_partial_invoice/sale_view.xml 1970-01-01 00:00:00 +0000
334+++ sale_order_partial_invoice/sale_view.xml 2013-12-17 14:13:31 +0000
335@@ -0,0 +1,59 @@
336+<?xml version="1.0" encoding="utf-8"?>
337+<openerp>
338+ <data>
339+
340+ <record id="view_order_line_tree" model="ir.ui.view">
341+ <field name="name">sale.order.line.tree</field>
342+ <field name="model">sale.order.line</field>
343+ <field name="inherit_id" ref="sale.view_order_line_tree"/>
344+ <field name="arch" type="xml">
345+ <field name="product_uom_qty" position="after">
346+ <field name="qty_invoiced" string="Inv. Qty"/>
347+ <field name="qty_delivered" string="Shipped Qty"/>
348+ </field>
349+ </field>
350+ </record>
351+
352+ <record id="view_order_line_form2" model="ir.ui.view">
353+ <field name="name">sale.order.line.form2</field>
354+ <field name="model">sale.order.line</field>
355+ <field name="inherit_id" ref="sale.view_order_line_form2"/>
356+ <field name="arch" type="xml">
357+ <xpath expr="//field[@name='product_uom_qty']/.." position="after">
358+ <field name="qty_invoiced" />
359+ <field name="qty_delivered"/>
360+ </xpath>
361+ </field>
362+ </record>
363+
364+ <record id="view_sale_order_invoice_partially_form" model="ir.ui.view">
365+ <field name="name">sale.order.line.invoice.partially.form</field>
366+ <field name="model">sale.order.line.invoice.partially</field>
367+ <field name="arch" type="xml">
368+ <form version="7.0" string="Invoice Sale Order Lines">
369+ <sheet>
370+ <separator string="Invoice lines"/>
371+ <group>
372+ <field name="line_ids" nolabel="1">
373+ <tree version="7.0" string="Lines" create="false" editable="bottom">
374+ <field name="sale_order_line_id" invisible="1"/>
375+ <field name="name"/>
376+ <field name="order_qty"/>
377+ <field name="qty_invoiced"/>
378+ <field name="qty_delivered"/>
379+ <field name="quantity"/>
380+ </tree>
381+ </field>
382+ </group>
383+ </sheet>
384+ <footer>
385+ <button name="create_invoice" type="object" string="Create Invoice" class="oe_highlight"/>
386+ or
387+ <button string="Cancel" class="oe_link" special="cancel" />
388+ </footer>
389+ </form>
390+ </field>
391+ </record>
392+
393+ </data>
394+</openerp>
395
396=== added directory 'sale_order_partial_invoice/test'
397=== added file 'sale_order_partial_invoice/test/make_remaining_invoice.yml'
398--- sale_order_partial_invoice/test/make_remaining_invoice.yml 1970-01-01 00:00:00 +0000
399+++ sale_order_partial_invoice/test/make_remaining_invoice.yml 2013-12-17 14:13:31 +0000
400@@ -0,0 +1,169 @@
401+-
402+ Create stockable products for testing purpose
403+-
404+ !record {model: product.product, id: product_stockable}:
405+ name: 'LCD Projector'
406+ list_price: 1234.99
407+ type: product
408+ procure_method: 'make_to_stock'
409+-
410+ !record {model: product.product, id: product_service}:
411+ name: 'LCD Projector 3 Year Warranty'
412+ list_price: 299.99
413+ type: service
414+ procure_method: 'make_to_stock'
415+-
416+ !record {model: product.product, id: product_consumable}:
417+ name: 'Spare screws'
418+ list_price: 1.22
419+ type: consu
420+ procure_method: 'make_to_stock'
421+-
422+ Create stock for the stockable product
423+-
424+ !record {model: stock.move, id: move1}:
425+ location_id: stock.stock_location_suppliers
426+ location_dest_id: stock.stock_location_stock
427+ product_id: product_stockable
428+ product_qty: 5
429+ state: done
430+-
431+ Create a sale order for the above
432+-
433+ !record {model: sale.order, id: sale_order2}:
434+ partner_id: base.res_partner_2
435+ order_policy: manual
436+ order_line:
437+ - product_id: product_stockable
438+ product_uom_qty: 3
439+ - product_id: product_service
440+ product_uom_qty: 3
441+ - product_id: product_consumable
442+ product_uom_qty: 12
443+-
444+ Confirm Order
445+-
446+ !workflow {model: sale.order, action: order_confirm, ref: sale_order2}
447+-
448+ Partial delivery
449+-
450+ !python {model: stock.picking}: |
451+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2"))])
452+ first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
453+ if first_picking.force_assign(cr, uid, first_picking):
454+ assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
455+ partial_moves = {}
456+ for move in first_picking.move_lines:
457+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
458+ first_picking.do_partial(partial_moves, context=context)
459+-
460+ Create partial invoice
461+-
462+ !python {model: sale.order.line.invoice.partially}: |
463+ sale_obj = self.pool['sale.order']
464+ sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order2"), context=context)
465+ wiz_data = {'name': 'wiz1',
466+ 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
467+ (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
468+ (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
469+ ],
470+ }
471+ id = self.create(cr, uid, wiz_data, context=context)
472+ self.create_invoice(cr, uid, [id], context=context)
473+-
474+ Check the 1st invoice is created
475+-
476+ !python {model: sale.order}: |
477+ order = self.browse(cr, uid, ref('sale_order2'))
478+ assert len(order.invoice_ids) == 1, "I should have 1 invoice"
479+-
480+ Check qty_invoiced 4
481+-
482+ !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
483+ - order_line[0].qty_invoiced == 1
484+ - order_line[1].qty_invoiced == 1
485+ - order_line[2].qty_invoiced == 4
486+-
487+ Check qty_delivered 4
488+-
489+ !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
490+ - order_line[0].qty_delivered == 1
491+ - order_line[1].qty_delivered == 1
492+ - order_line[2].qty_delivered == 4
493+-
494+ Partial delivery 2
495+-
496+ !python {model: stock.picking}: |
497+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2")), ('state', '!=', 'done')])
498+ picking = self.browse(cr, uid, delivery_orders[-1], context=context)
499+ if picking.force_assign(cr, uid, picking):
500+ assert len(picking.move_lines) == 2, "wrong count of stock moves"
501+ partial_moves = {}
502+ for move in picking.move_lines:
503+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
504+ picking.do_partial(partial_moves, context=context)
505+-
506+ I invoice the remaining of the SO
507+-
508+ !python {model: sale.advance.payment.inv}: |
509+ ctx = context.copy()
510+ ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order2")], "active_id":ref("sale_order2")})
511+ pay_id = self.create(cr, uid, {'advance_payment_method': 'all'})
512+ self.create_invoices(cr, uid, [pay_id], context=ctx)
513+-
514+ Check the invoiced qtys in the invoice
515+-
516+ !python {model: sale.order}: |
517+ order = self.browse(cr, uid, ref('sale_order2'))
518+ assert len(order.invoice_ids) == 2, "I should have 2 invoices"
519+-
520+ Check qty_invoiced 5
521+-
522+ !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
523+ - order_line[0].qty_invoiced == 3
524+ - order_line[1].qty_invoiced == 3
525+ - order_line[2].qty_invoiced == 12
526+-
527+ Check qty_delivered 5
528+-
529+ !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
530+ - order_line[0].qty_delivered == 3
531+ - order_line[1].qty_delivered == 3
532+ - order_line[2].qty_delivered == 12
533+-
534+ I confirm the invoices
535+-
536+ !python {model: sale.order}: |
537+ import netsvc
538+ wf_service = netsvc.LocalService("workflow")
539+ so = self.browse(cr, uid, ref("sale_order2"))
540+ for invoice in so.invoice_ids:
541+ wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
542+-
543+ I pay the invoices.
544+-
545+ !python {model: account.invoice}: |
546+ sale_order = self.pool.get('sale.order')
547+ order = sale_order.browse(cr, uid, ref("sale_order2"))
548+ journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
549+ for invoice in order.invoice_ids:
550+ invoice.pay_and_reconcile(
551+ invoice.amount_total, ref('account.cash'), ref('account.period_8'),
552+ journal_ids[0], ref('account.cash'),
553+ ref('account.period_8'), journal_ids[0],
554+ name='test')
555+-
556+ I run the scheduler (required to get the correct sale order state)
557+-
558+ !python {model: procurement.order}: |
559+ self.run_scheduler(cr, uid)
560+-
561+ I check sale order
562+-
563+ !python {model: sale.order}: |
564+ sale_order = self.browse(cr, uid, ref("sale_order2"))
565+ assert sale_order.invoice_ids, "Invoice should be created."
566+ assert sale_order.invoice_exists, "Order is not invoiced."
567+ assert sale_order.invoiced, "Order is not paid."
568+ assert sale_order.state == 'done', 'Order should be Done.'
569+
570
571=== added file 'sale_order_partial_invoice/test/partial_so_invoice.yml'
572--- sale_order_partial_invoice/test/partial_so_invoice.yml 1970-01-01 00:00:00 +0000
573+++ sale_order_partial_invoice/test/partial_so_invoice.yml 2013-12-17 14:13:31 +0000
574@@ -0,0 +1,231 @@
575+-
576+ Create stockable products for testing purpose
577+-
578+ !record {model: product.product, id: product_stockable}:
579+ name: 'LCD Projector'
580+ list_price: 1234.99
581+ type: product
582+ procure_method: 'make_to_stock'
583+-
584+ !record {model: product.product, id: product_service}:
585+ name: 'LCD Projector 3 Year Warranty'
586+ list_price: 299.99
587+ type: service
588+ procure_method: 'make_to_stock'
589+-
590+ !record {model: product.product, id: product_consumable}:
591+ name: 'Spare screws'
592+ list_price: 1.22
593+ type: consu
594+ procure_method: 'make_to_stock'
595+-
596+ Create stock for the stockable product
597+-
598+ !record {model: stock.move, id: move1}:
599+ location_id: stock.stock_location_suppliers
600+ location_dest_id: stock.stock_location_stock
601+ product_id: product_stockable
602+ product_qty: 5
603+ state: done
604+-
605+ Create a sale order for the above
606+-
607+ !record {model: sale.order, id: sale_order1}:
608+ partner_id: base.res_partner_2
609+ order_policy: manual
610+ order_line:
611+ - product_id: product_stockable
612+ product_uom_qty: 3
613+ - product_id: product_service
614+ product_uom_qty: 3
615+ - product_id: product_consumable
616+ product_uom_qty: 12
617+-
618+ Check qty_invoiced 1
619+-
620+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
621+ - order_line[0].qty_invoiced == 0
622+ - order_line[1].qty_invoiced == 0
623+ - order_line[2].qty_invoiced == 0
624+-
625+ Check qty_delivered 1
626+-
627+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
628+ - order_line[0].qty_delivered == 0
629+ - order_line[1].qty_delivered == 0
630+ - order_line[2].qty_delivered == 0
631+-
632+ Confirm Order
633+-
634+ !workflow {model: sale.order, action: order_confirm, ref: sale_order1}
635+-
636+ Check qty_invoiced 2
637+-
638+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
639+ - order_line[0].qty_invoiced == 0
640+ - order_line[1].qty_invoiced == 0
641+ - order_line[2].qty_invoiced == 0
642+-
643+ Check qty_delivered 2
644+-
645+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
646+ - order_line[0].qty_delivered == 0
647+ - order_line[1].qty_delivered == 0
648+ - order_line[2].qty_delivered == 0
649+-
650+ Partial delivery
651+-
652+ !python {model: stock.picking}: |
653+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1"))])
654+ first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
655+ if first_picking.force_assign(cr, uid, first_picking):
656+ assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
657+ partial_moves = {}
658+ for move in first_picking.move_lines:
659+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
660+ first_picking.do_partial(partial_moves, context=context)
661+-
662+ Check qty_invoiced 3
663+-
664+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
665+ - order_line[0].qty_invoiced == 0
666+ - order_line[1].qty_invoiced == 0
667+ - order_line[2].qty_invoiced == 0
668+-
669+ Check qty_delivered 3
670+-
671+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
672+ - order_line[0].qty_delivered == 1
673+ - order_line[1].qty_delivered == 0
674+ - order_line[2].qty_delivered == 4
675+-
676+ Check SO state
677+-
678+ !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
679+ - state == 'manual'
680+-
681+ Create partial invoice
682+-
683+ !python {model: sale.order.line.invoice.partially}: |
684+ sale_obj = self.pool['sale.order']
685+ sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order1"), context=context)
686+ wiz_data = {'name': 'wiz1',
687+ 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
688+ (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
689+ (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
690+ ],
691+ }
692+ id = self.create(cr, uid, wiz_data, context=context)
693+ self.create_invoice(cr, uid, [id], context=context)
694+-
695+ Check the 1st invoice is created
696+-
697+ !python {model: sale.order}: |
698+ order = self.browse(cr, uid, ref('sale_order1'))
699+ assert len(order.invoice_ids) == 1, "I should have 1 invoice"
700+-
701+ Check qty_invoiced 4
702+-
703+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
704+ - order_line[0].qty_invoiced == 1
705+ - order_line[1].qty_invoiced == 1
706+ - order_line[2].qty_invoiced == 4
707+-
708+ Check qty_delivered 4
709+-
710+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
711+ - order_line[0].qty_delivered == 1
712+ - order_line[1].qty_delivered == 1
713+ - order_line[2].qty_delivered == 4
714+-
715+ Check SO state
716+-
717+ !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
718+ - state == 'manual'
719+-
720+ Partial delivery 2
721+-
722+ !python {model: stock.picking}: |
723+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1")), ('state', '!=', 'done')])
724+ picking = self.browse(cr, uid, delivery_orders[-1], context=context)
725+ if picking.force_assign(cr, uid, picking):
726+ assert len(picking.move_lines) == 2, "wrong count of stock moves"
727+ partial_moves = {}
728+ for move in picking.move_lines:
729+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
730+ picking.do_partial(partial_moves, context=context)
731+-
732+ Create partial invoice 2
733+-
734+ !python {model: sale.order.line.invoice.partially}: |
735+ sale_obj = self.pool['sale.order']
736+ sale = sale_obj.browse(cr, uid, ref("sale_order1"), context=context)
737+ wiz_data = {'name': 'wiz2',
738+ 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 2}),
739+ (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 2}),
740+ (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 8}),
741+ ],
742+ }
743+ id = self.create(cr, uid, wiz_data, context=context)
744+ self.create_invoice(cr, uid, [id], context=context)
745+-
746+ Check the 2nd invoice is created
747+-
748+ !python {model: sale.order}: |
749+ order = self.browse(cr, uid, ref('sale_order1'))
750+ assert len(order.invoice_ids) == 2, "I should have 2 invoices"
751+-
752+ Check qty_invoiced 5
753+-
754+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
755+ - order_line[0].qty_invoiced == 3
756+ - order_line[1].qty_invoiced == 3
757+ - order_line[2].qty_invoiced == 12
758+-
759+ Check qty_delivered 5
760+-
761+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
762+ - order_line[0].qty_delivered == 3
763+ - order_line[1].qty_delivered == 3
764+ - order_line[2].qty_delivered == 12
765+-
766+ Check SO state
767+-
768+ !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
769+ - state == 'manual'
770+-
771+ I confirm the invoices
772+-
773+ !python {model: sale.order}: |
774+ import netsvc
775+ wf_service = netsvc.LocalService("workflow")
776+ so = self.browse(cr, uid, ref("sale_order1"))
777+ for invoice in so.invoice_ids:
778+ wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
779+-
780+ I pay the invoices.
781+-
782+ !python {model: account.invoice}: |
783+ sale_order = self.pool.get('sale.order')
784+ order = sale_order.browse(cr, uid, ref("sale_order1"))
785+ journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
786+ for invoice in order.invoice_ids:
787+ invoice.pay_and_reconcile(
788+ invoice.amount_total, ref('account.cash'), ref('account.period_8'),
789+ journal_ids[0], ref('account.cash'),
790+ ref('account.period_8'), journal_ids[0],
791+ name='test')
792+-
793+ I run the scheduler (required to get the correct sale order state)
794+-
795+ !python {model: procurement.order}: |
796+ self.run_scheduler(cr, uid)
797+-
798+ I check sale order
799+-
800+ !python {model: sale.order}: |
801+ sale_order = self.browse(cr, uid, ref("sale_order1"))
802+ assert sale_order.invoice_ids, "Invoice should be created."
803+ assert sale_order.invoice_exists, "Order is not invoiced."
804+ assert sale_order.invoiced, "Order is not paid."
805+ assert sale_order.state == 'done', 'Order should be Done.'

Subscribers

People subscribed via source and target branches