diff --git a/addons/blanket_so_po/__init__.py b/addons/blanket_so_po/__init__.py
new file mode 100755
index 00000000..c73120bb
--- /dev/null
+++ b/addons/blanket_so_po/__init__.py
@@ -0,0 +1,4 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing details.
+
+from . import wizard
+from . import models
diff --git a/addons/blanket_so_po/__manifest__.py b/addons/blanket_so_po/__manifest__.py
new file mode 100644
index 00000000..5fae39ea
--- /dev/null
+++ b/addons/blanket_so_po/__manifest__.py
@@ -0,0 +1,26 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Blanket Sale Order/Purchase Order',
+ 'version': "1.0",
+ 'category': 'Sales and Purchase Management',
+ 'summary': 'A Blanket Sales/Purchase Order clearly lays out the terms '
+ 'and conditions of a Sales/Purchase including quantities '
+ 'required and when they are to be delivered.',
+ "author": "Flectra",
+ 'website': 'https://flectrahq.com',
+ 'depends': ['purchase', 'sale_stock'],
+ 'data': [
+ 'wizard/transfer_so_products_view.xml',
+ 'wizard/transfer_po_products_view.xml',
+ 'views/sale_view.xml',
+ 'views/purchase_view.xml',
+ ],
+ 'demo': [
+ 'demo/blanket_sale_demo.xml',
+ 'demo/blanket_purchase_demo.xml'
+ ],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+}
diff --git a/addons/blanket_so_po/demo/blanket_purchase_demo.xml b/addons/blanket_so_po/demo/blanket_purchase_demo.xml
new file mode 100644
index 00000000..7f605db9
--- /dev/null
+++ b/addons/blanket_so_po/demo/blanket_purchase_demo.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+ [RAM-SR5] RAM DDR SR5
+
+
+
+ 6000
+ 10
+
+
+
+
+
+ [M-Wir] Mouse, Wireless]
+
+
+
+ 200
+ 5
+
+
+
+
+
+ [MBi9] Motherboard I9P57
+
+
+
+ 3000
+ 8
+
+
+
+
diff --git a/addons/blanket_so_po/demo/blanket_sale_demo.xml b/addons/blanket_so_po/demo/blanket_sale_demo.xml
new file mode 100644
index 00000000..7cb790fd
--- /dev/null
+++ b/addons/blanket_so_po/demo/blanket_sale_demo.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Laptop E5023
+
+ 10
+
+ 2950.00
+
+
+
+
+
+ Headset USB
+
+ 3
+
+ 2950.00
+
+
+
+
+
+ Webcamv
+
+ 10
+
+ 45.00
+
+
+
+
diff --git a/addons/blanket_so_po/models/__init__.py b/addons/blanket_so_po/models/__init__.py
new file mode 100644
index 00000000..76c0748f
--- /dev/null
+++ b/addons/blanket_so_po/models/__init__.py
@@ -0,0 +1,5 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+from . import sale
+from . import purchase
diff --git a/addons/blanket_so_po/models/purchase.py b/addons/blanket_so_po/models/purchase.py
new file mode 100644
index 00000000..b2eacfc6
--- /dev/null
+++ b/addons/blanket_so_po/models/purchase.py
@@ -0,0 +1,66 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+from flectra import api, fields, models, _
+from flectra.exceptions import Warning
+
+
+class PurchaseOrder(models.Model):
+ _inherit = "purchase.order"
+
+ @api.multi
+ def button_cancel(self):
+ res = super(PurchaseOrder, self).button_cancel()
+ if self.order_line.filtered(
+ lambda l: l.blanket_po_line):
+ raise Warning(
+ _('Sorry, You can not cancel blanket line based PO.'))
+ self.write({'state': 'cancel'})
+ return res
+
+
+class PurchaseOrderLine(models.Model):
+ _inherit = 'purchase.order.line'
+
+ @api.multi
+ def _prepare_stock_moves(self, picking):
+ res = super(PurchaseOrderLine, self)._prepare_stock_moves(picking)
+ self.ensure_one()
+ context = dict(self._context)
+ if self.product_id.type not in ['product',
+ 'consu'] or self.blanket_po_line \
+ and not context.get('blanket'):
+ return []
+ qty = 0.0
+ for move in self.move_ids.filtered(
+ lambda x: x.state != 'cancel' and not
+ x.location_dest_id.usage == "supplier"):
+ qty += move.product_uom._compute_quantity(
+ move.product_uom_qty, self.product_uom,
+ rounding_method='HALF-UP')
+ for re in res:
+ if self.blanket_po_line and context.get('transfer_qty'):
+ re['product_uom_qty'] = context.get('transfer_qty')
+ else:
+ re['product_uom_qty'] = self.product_qty - qty
+ return res
+
+ blanket_po_line = fields.Boolean(string="Blanket Order", copy=False)
+ remaining_to_po_transfer = fields.Float(string="Remaining to Transfer",
+ copy=False)
+
+ @api.multi
+ def create(self, vals):
+ if vals.get('product_qty') and vals.get('blanket_po_line'):
+ vals.update(
+ {'remaining_to_po_transfer': vals.get('product_qty')})
+ res = super(PurchaseOrderLine, self).create(vals)
+ return res
+
+ @api.multi
+ def write(self, values):
+ result = super(PurchaseOrderLine, self).write(values)
+ for line in self:
+ if 'product_qty' and 'blanket_po_line' in values:
+ line.remaining_to_po_transfer = line.product_qty
+ return result
diff --git a/addons/blanket_so_po/models/sale.py b/addons/blanket_so_po/models/sale.py
new file mode 100644
index 00000000..5973cab1
--- /dev/null
+++ b/addons/blanket_so_po/models/sale.py
@@ -0,0 +1,114 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+from flectra import api, models, fields
+from flectra.exceptions import UserError
+from flectra.tools import float_compare
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+ _rec_name = 'product_id'
+
+ blanket_so_line = fields.Boolean(string="Blanket Order", copy=False)
+ remaining_to_so_transfer = fields.Float(string="Remaining to Transfer",
+ copy=False)
+
+ @api.multi
+ def _action_launch_procurement_rule(self):
+ """
+ Launch procurement group run method with required/custom fields
+ genrated by a
+ sale order line. procurement group will launch '_run_move',
+ '_run_buy' or '_run_manufacture'
+ depending on the sale order line product rule.
+ """
+ precision = self.env['decimal.precision'].precision_get(
+ 'Product Unit of Measure')
+ errors = []
+ context = dict(self._context)
+ for line in self:
+ if line.state != 'sale' or not line.product_id.type in (
+ 'consu', 'product') or line.blanket_so_line and \
+ not context.get('blanket'):
+ continue
+ qty = 0.0
+ for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
+ qty += move.product_uom._compute_quantity(move.product_uom_qty,
+ line.product_uom,
+ rounding_method='HALF-UP')
+ if float_compare(qty, line.product_uom_qty,
+ precision_digits=precision) >= 0:
+ continue
+
+ group_id = line.order_id.procurement_group_id
+ if not group_id:
+ group_id = self.env['procurement.group'].create({
+ 'name': line.order_id.name,
+ 'move_type': line.order_id.picking_policy,
+ 'sale_id': line.order_id.id,
+ 'partner_id': line.order_id.partner_shipping_id.id,
+ })
+ line.order_id.procurement_group_id = group_id
+ else:
+ # In case the procurement group is already created and the
+ # order was
+ # cancelled, we need to update certain values of the group.
+ updated_vals = {}
+ if group_id.partner_id != line.order_id.partner_shipping_id:
+ updated_vals.update(
+ {'partner_id': line.order_id.partner_shipping_id.id})
+ if group_id.move_type != line.order_id.picking_policy:
+ updated_vals.update(
+ {'move_type': line.order_id.picking_policy})
+ if updated_vals:
+ group_id.write(updated_vals)
+
+ values = line._prepare_procurement_values(group_id=group_id)
+
+ if line.blanket_so_line and context.get('blanket'):
+ product_qty = context.get('transfer_qty')
+ else:
+ product_qty = line.product_uom_qty - qty
+
+ procurement_uom = line.product_uom
+ quant_uom = line.product_id.uom_id
+ get_param = self.env['ir.config_parameter'].sudo().get_param
+ if procurement_uom.id != quant_uom.id and get_param(
+ 'stock.propagate_uom') != '1':
+ product_qty = line.product_uom._compute_quantity(product_qty,
+ quant_uom,
+ rounding_method='HALF-UP')
+ procurement_uom = quant_uom
+
+ try:
+ self.env['procurement.group'].run(line.product_id, product_qty,
+ procurement_uom,
+ line.order_id.partner_shipping_id.property_stock_customer,
+ line.name,
+ line.order_id.name, values)
+ except UserError as error:
+ errors.append(error.name)
+ if errors:
+ raise UserError('\n'.join(errors))
+ return True
+
+ @api.multi
+ def create(self, vals):
+ if vals.get('product_uom_qty') and vals.get('blanket_so_line'):
+ vals.update(
+ {'remaining_to_so_transfer': vals.get('product_uom_qty')})
+ res = super(SaleOrderLine, self).create(vals)
+ return res
+
+ @api.multi
+ def write(self, values):
+ result = super(SaleOrderLine, self).write(values)
+ for line in self:
+ if 'product_uom_qty' and 'blanket_so_line' in values:
+ line.remaining_to_so_transfer = line.product_uom_qty
+ return result
diff --git a/addons/blanket_so_po/static/description/icon.png b/addons/blanket_so_po/static/description/icon.png
new file mode 100644
index 00000000..3dc4657f
Binary files /dev/null and b/addons/blanket_so_po/static/description/icon.png differ
diff --git a/addons/blanket_so_po/tests/__init__.py b/addons/blanket_so_po/tests/__init__.py
new file mode 100644
index 00000000..61859895
--- /dev/null
+++ b/addons/blanket_so_po/tests/__init__.py
@@ -0,0 +1,4 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing details.
+
+from . import test_purchase_to_invoice_and_delivery
+from . import test_sale_to_invoice_and_delivery
diff --git a/addons/blanket_so_po/tests/test_purchase_to_invoice_and_delivery.py b/addons/blanket_so_po/tests/test_purchase_to_invoice_and_delivery.py
new file mode 100644
index 00000000..f1d1edc7
--- /dev/null
+++ b/addons/blanket_so_po/tests/test_purchase_to_invoice_and_delivery.py
@@ -0,0 +1,175 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+import logging
+
+from flectra.exceptions import Warning
+from flectra.tests.common import TransactionCase
+
+
+class TestPurchaseOrder(TransactionCase):
+ def setUp(self):
+ super(TestPurchaseOrder, self).setUp()
+ self.stock_move = self.env['stock.move']
+ self.inv_obj = self.env['account.invoice']
+ self.purchase_wizard = self.env['purchase.transfer.products']
+ self.po1 = self.env.ref('blanket_so_po.blanket_purchase_order')
+ self.po_line_with_blanket = self.env.ref(
+ 'blanket_so_po.blanket_purchase_order_line_1')
+ self.po_line_without_blanket = self.env.ref(
+ 'blanket_so_po.blanket_purchase_order_line_2')
+ self.po_line_without_blanket2 = self.env.ref(
+ 'blanket_so_po.blanket_purchase_order_line_3')
+
+ def test_2_purchase_with_blanket(self):
+ self.assertTrue(self.po1, 'Purchase: no purchase order created')
+ self.po1.button_confirm()
+ self.assertEqual(self.po1.state, 'purchase',
+ 'Purchase: PO state should be "Purchase"')
+ self.assertTrue(self.po1.picking_ids, "Picking should be created.")
+ logging.info('Test Cases for Blanket Purchase order')
+ logging.info('Purchase Order - %s' % (self.po1.name))
+ logging.info(
+ '============================================================='
+ '==================+=====')
+ logging.info(
+ ' | Blanket Po Line | Product | Ordered Qty | Remaining '
+ 'to transfer | Received Qty |')
+ for line in self.po1.order_line:
+ logging.info(
+ ' %s | %s | %d | %d '
+ ' | %d ' % (
+ line.blanket_po_line, line.product_id.name,
+ line.product_qty,
+ line.remaining_to_po_transfer, line.qty_received))
+ logging.info(
+ '========================================================='
+ '========================')
+ #
+ blanket_lines_po = self.po1.order_line.search(
+ [('order_id', '=', self.po1.id),
+ ('blanket_po_line', '=', True)])
+ uom_qty = self.po_line_with_blanket.product_qty
+ transfer_qty = 2
+ remaining_qty = 0.0
+
+ len_blanket_lines = len(blanket_lines_po)
+ total_po_line = len(self.po1.order_line)
+ move_lines = len(self.po1.picking_ids.move_lines)
+ logging.info(
+ '*****************************************************')
+ logging.info('Delivery Order for Purchase Order- %s' % (
+ self.po1.picking_ids.name))
+ logging.info(
+ '============================================================'
+ '===================+=====')
+ logging.info(
+ ' | Product | Initial Demand | Reserved | Done |')
+ for move in self.po1.picking_ids.move_lines:
+ logging.info('| %s | %d |%d | %d' % (
+ move.product_id.name, move.product_uom_qty,
+ move.reserved_availability, move.quantity_done))
+ logging.info(
+ '========================================================'
+ '=========================')
+ self.assertEqual(move_lines, total_po_line - len_blanket_lines,
+ 'There is no equal number of move lines in move')
+
+ self.assertTrue(self.po_line_with_blanket.blanket_po_line,
+ 'Purchase: There is a Blanket po line')
+
+ remaining_qty = uom_qty - transfer_qty
+ transfer_wizard = self.purchase_wizard.create(
+ {'ref_id': self.po_line_with_blanket.id,
+ 'transfer_qty': transfer_qty})
+ transfer_wizard.split_qty_wt_newline_po()
+ self.assertEqual(remaining_qty, uom_qty - transfer_qty,
+ 'Remaining to transfer qty is different')
+
+ total_po_line = len(self.po1.order_line)
+
+ transfer_qty += 5
+ remaining_qty = uom_qty - transfer_qty
+ transfer_wizard = self.purchase_wizard.create(
+ {'ref_id': self.po_line_with_blanket.id, 'transfer_qty': 5})
+ transfer_wizard.split_qty_wt_newline_po()
+ self.assertEqual(remaining_qty, uom_qty - transfer_qty,
+ 'Remaining to transfer qty is different')
+
+ logging.info(
+ '*****************************************************')
+ logging.info(
+ 'Purchase Order after Blanket Split lines- %s' % (
+ self.po1.name))
+ logging.info(
+ '============================================================'
+ '===================+=====')
+ logging.info(
+ ' | Blanket Po Line | Product | Ordered Qty | Remaining '
+ 'to transfer | Received Qty |')
+ for line in self.po1.order_line:
+ logging.info(
+ ' %s | %s | %d | %d '
+ ' | %d ' % (
+ line.blanket_po_line, line.product_id.name,
+ line.product_qty,
+ line.remaining_to_po_transfer, line.qty_received))
+ logging.info(
+ '========================================================='
+ '========================')
+
+ with self.assertRaises(Warning):
+ self.po1.button_cancel()
+
+ self.picking = self.po1.picking_ids[0]
+ self.assertEqual(self.picking.move_lines[-1].quantity_done, 0.0)
+ self.assertEqual(self.picking.move_lines[-2].quantity_done, 0.0)
+ self.picking.move_lines[-1].quantity_done = 2
+
+ self.po1._compute_picking()
+ self.po1._compute_is_shipped()
+
+ logging.info(
+ '*****************************************************')
+ logging.info('Delivery Order - %s' % (self.picking.name))
+ logging.info(
+ '========================================================='
+ '======================+=====')
+ logging.info(
+ ' id | Product | Initial Demand | Reserved | Done |')
+ for move in self.picking.move_lines:
+ logging.info(
+ '%d | %s | %d |%d | %d' % (
+ move.id, move.product_id.name, move.product_uom_qty,
+ move.reserved_availability, move.quantity_done))
+ logging.info(
+ '========================================================='
+ '========================')
+
+ self.picking.force_assign()
+ res_dict = self.picking.button_validate()
+ backorder_wizard = self.env[(res_dict.get('res_model'))].browse(
+ res_dict.get('res_id'))
+ backorder_wizard.process()
+ self.picking.action_done()
+
+ logging.info(
+ '*****************************************************')
+ logging.info('Purchase Order after validate Delivery order- %s' % (
+ self.po1.name))
+ logging.info(
+ '============================================================'
+ '===================+=====')
+ logging.info(
+ ' | Blanket Po Line | Product | Ordered Qty | Remaining '
+ 'to transfer | Received Qty |')
+ for line in self.po1.order_line:
+ logging.info(
+ ' %s | %s | %d | %d '
+ ' | %d ' % (
+ line.blanket_po_line, line.product_id.name,
+ line.product_qty,
+ line.remaining_to_po_transfer, line.qty_received))
+ logging.info(
+ '========================================================='
+ '========================')
diff --git a/addons/blanket_so_po/tests/test_sale_to_invoice_and_delivery.py b/addons/blanket_so_po/tests/test_sale_to_invoice_and_delivery.py
new file mode 100644
index 00000000..1920d3ab
--- /dev/null
+++ b/addons/blanket_so_po/tests/test_sale_to_invoice_and_delivery.py
@@ -0,0 +1,180 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+import logging
+
+from flectra.tests.common import TransactionCase
+
+
+class TestSaleOrder(TransactionCase):
+ def setUp(self):
+ super(TestSaleOrder, self).setUp()
+ self.so_model = self.env['sale.order']
+ self.so_line_model = self.env['sale.order.line']
+ self.stock_picking_model = self.env['stock.picking']
+ self.stock_move_model = self.env['stock.move']
+ self.stock_location_model = self.env['stock.location']
+ self.sale_wizard = self.env['sale.transfer.products']
+ self.invoice_wizard = self.env['sale.advance.payment.inv']
+ self.sale1 = self.env.ref('blanket_so_po.sale_order_blanket1')
+ self.so_line_with_blanket = self.env.ref(
+ 'blanket_so_po.sale_order_line_blanket_1')
+ self.so_line_without_blanket = self.env.ref(
+ 'blanket_so_po.sale_order_line_blanket_2')
+ self.so_line_without_blanket2 = self.env.ref(
+ 'blanket_so_po.sale_order_line_blanket_3')
+ self.inv_obj = self.env['account.invoice']
+
+ def test_1_sale_with_blanket(self):
+ self.sale1.force_quotation_send()
+ self.sale1.action_confirm()
+ self.assertTrue(self.sale1.state, 'sale')
+ self.assertTrue(self.sale1.invoice_status, 'to invoice')
+ logging.info('Test Cases for Blanket Sale order')
+ logging.info('Sale Order - %s' % (self.sale1.name))
+ logging.info(
+ '============================================================'
+ '===================+=====')
+ logging.info(
+ ' | Blanket So Line | Product | Ordered Qty | Remaining '
+ 'to transfer | Delivered |')
+ for line in self.sale1.order_line:
+ logging.info(
+ ' %s | %s | %d | %d '
+ ' | %d ' % (
+ line.blanket_so_line, line.product_id.name,
+ line.product_uom_qty, line.remaining_to_so_transfer,
+ line.qty_delivered))
+ logging.info(
+ '========================================================'
+ '=========================')
+ uom_qty = self.so_line_with_blanket.product_uom_qty
+ transfer_qty = 2
+ remaining_qty = 0.0
+
+ logging.info(
+ '*****************************************************')
+ logging.info(
+ '========================================================='
+ '======================+=====')
+ logging.info(
+ ' | Product | Initial Demand | Reserved | Done |')
+ for picking in self.sale1.picking_ids:
+ for line in picking.move_lines:
+ logging.info('| %s | %d |%d | %d' % (
+ line.product_id.name, line.product_uom_qty,
+ line.reserved_availability, line.quantity_done))
+ logging.info(
+ '======================================================='
+ '==========================')
+
+ remaining_qty = uom_qty - transfer_qty
+ transfer_wizard = self.sale_wizard.create(
+ {'ref_id': self.so_line_with_blanket.id,
+ 'transfer_qty': transfer_qty})
+ transfer_wizard.split_qty_wt_newline()
+ self.assertEqual(remaining_qty, uom_qty - transfer_qty,
+ 'Remaining to transfer qty is different')
+
+ transfer_qty += 5
+ remaining_qty = uom_qty - transfer_qty
+ transfer_wizard = self.sale_wizard.create(
+ {'ref_id': self.so_line_with_blanket.id, 'transfer_qty': 5})
+ transfer_wizard.split_qty_wt_newline()
+ self.assertEqual(remaining_qty, uom_qty - transfer_qty,
+ 'Remaining to transfer qty is different')
+
+ logging.info(
+ '*****************************************************')
+ logging.info(
+ 'Sale Order after Blanket Split lines- %s' % (self.sale1.name))
+ logging.info(
+ '========================================================='
+ '======================+=====')
+ logging.info(
+ ' | Blanket So Line | Product | Ordered Qty | Remaining '
+ 'to transfer | Delivered |')
+ for line in self.sale1.order_line:
+ logging.info(
+ ' %s | %s | %d | %d '
+ ' | %d ' % (
+ line.blanket_so_line, line.product_id.name,
+ line.product_uom_qty, line.remaining_to_so_transfer,
+ line.qty_delivered))
+ logging.info(
+ '======================================================='
+ '==========================')
+
+ self.assertEqual(self.sale1.picking_ids.move_lines[-1].quantity_done,
+ 0.0)
+ self.assertEqual(self.sale1.picking_ids.move_lines[-2].quantity_done,
+ 0.0)
+ self.sale1.picking_ids.move_lines[-1].quantity_done = 2
+
+ logging.info(
+ '*****************************************************')
+ logging.info('Delivery Order - %s' % (self.sale1.picking_ids.name))
+ logging.info(
+ '==========================================================='
+ '====================+=====')
+ logging.info(
+ ' | Product | Initial Demand | Reserved | Done |')
+ for move in self.sale1.picking_ids.move_lines:
+ logging.info('| %s | %d |%d | %d' % (
+ move.product_id.name, move.product_uom_qty,
+ move.reserved_availability, move.quantity_done))
+ logging.info(
+ '======================================================='
+ '==========================')
+
+ self.sale1.picking_ids.action_confirm()
+ self.sale1.picking_ids.action_assign()
+ res_dict = self.sale1.picking_ids.button_validate()
+ backorder_wizard = self.env[(res_dict.get('res_model'))].browse(
+ res_dict.get('res_id'))
+ backorder_wizard.process()
+
+ logging.info(
+ '*****************************************************')
+ logging.info(
+ 'Sale Order after validate Delivery order- %s' % (
+ self.sale1.name))
+ logging.info(
+ '=========================================================='
+ '=====================+=====')
+ logging.info(
+ ' | Blanket So Line | Product | Ordered Qty | Remaining '
+ 'to transfer | Delivered |invoice status')
+ for line in self.sale1.order_line:
+ logging.info(
+ ' %s | %s | %d | %d '
+ ' | %d |%s' % (
+ line.blanket_so_line, line.product_id.name,
+ line.product_uom_qty, line.remaining_to_so_transfer,
+ line.qty_delivered, line.invoice_status))
+ logging.info(
+ '======================================================='
+ '==========================')
+
+ self.assertEqual(len(self.sale1.picking_ids), 2,
+ 'There is no 2 pickings are available')
+
+ context = {"active_model": 'sale.order',
+ "active_ids": [self.sale1.id],
+ "active_id": self.sale1.id}
+
+ for invoice in self.sale1.invoice_ids:
+ logging.info('Invoice of Delivered Quantity.')
+ for line in invoice.invoice_line_ids:
+ logging.info(
+ '==================================================')
+ logging.info(
+ '| Product | Quantity | Unit Price | Subtotal |')
+ logging.info(
+ '=================================================|')
+ logging.info(
+ '|%s |%d |%d |%d | ' % (
+ line.product_id.name, line.quantity,
+ line.price_unit,
+ line.price_subtotal))
+ invoice.with_context(context).invoice_validate()
diff --git a/addons/blanket_so_po/views/purchase_view.xml b/addons/blanket_so_po/views/purchase_view.xml
new file mode 100755
index 00000000..be0c59db
--- /dev/null
+++ b/addons/blanket_so_po/views/purchase_view.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+ inherit.purchase.form.view
+ purchase.order
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/blanket_so_po/views/sale_view.xml b/addons/blanket_so_po/views/sale_view.xml
new file mode 100755
index 00000000..7f05658a
--- /dev/null
+++ b/addons/blanket_so_po/views/sale_view.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+ inherit.sale.order.form.view
+ sale.order
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/blanket_so_po/wizard/__init__.py b/addons/blanket_so_po/wizard/__init__.py
new file mode 100755
index 00000000..c4e49532
--- /dev/null
+++ b/addons/blanket_so_po/wizard/__init__.py
@@ -0,0 +1,6 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+
+from . import transfer_po_products
+from . import transfer_so_products
diff --git a/addons/blanket_so_po/wizard/transfer_po_products.py b/addons/blanket_so_po/wizard/transfer_po_products.py
new file mode 100644
index 00000000..7e5954c6
--- /dev/null
+++ b/addons/blanket_so_po/wizard/transfer_po_products.py
@@ -0,0 +1,56 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+from flectra import api, fields, models, _
+from flectra.exceptions import ValidationError
+
+
+class PurchaseTransferProducts(models.TransientModel):
+ _name = 'purchase.transfer.products'
+ _description = 'Transfer products from Purchase Lines'
+
+ transfer_qty = fields.Float("Transfer Qty")
+ ref_id = fields.Many2one('purchase.order.line',
+ string="Product Reference",
+ readonly=True)
+
+ @api.model
+ def default_get(self, fields):
+ context = dict(self._context)
+ result = super(PurchaseTransferProducts, self).default_get(
+ fields)
+ purchase_line = self.env['purchase.order.line'].browse(
+ context.get('active_id'))
+ result.update({
+ 'ref_id': purchase_line.id or False,
+ 'transfer_qty': purchase_line.remaining_to_po_transfer})
+ return result
+
+ @api.multi
+ def split_qty_wt_newline_po(self):
+ self.ensure_one()
+ if self.ref_id.remaining_to_po_transfer < self.transfer_qty:
+ raise ValidationError(_(
+ 'Sorry, You can not transfer more than requested or '
+ 'remains!'))
+ elif self.transfer_qty <= 0:
+ raise ValidationError(_(
+ 'Sorry, You can not transfer zero or negative '
+ 'quantity!'))
+ self.ref_id._create_or_update_picking()
+ pickings = self.ref_id.order_id.picking_ids.filtered(
+ lambda p: p.state not in ('done', 'cancel'))
+ self.ref_id.remaining_to_po_transfer -= self.transfer_qty
+ for picking in pickings:
+ for line in picking.move_lines.filtered(
+ lambda l: l.product_id.id == self.ref_id.product_id.id and
+ l.purchase_line_id.id == self.ref_id.id):
+ total_product_qty = \
+ self.ref_id.remaining_to_po_transfer + \
+ self.ref_id.qty_received
+ line.product_uom_qty = \
+ self.ref_id.product_qty - total_product_qty
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
diff --git a/addons/blanket_so_po/wizard/transfer_po_products_view.xml b/addons/blanket_so_po/wizard/transfer_po_products_view.xml
new file mode 100755
index 00000000..f4ade64f
--- /dev/null
+++ b/addons/blanket_so_po/wizard/transfer_po_products_view.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ purchase.transfer.products.form.view
+ purchase.transfer.products
+
+
+
+
+
+
+ Transfer Products
+ purchase.transfer.products
+ form
+ form
+
+ new
+
+
+
+
diff --git a/addons/blanket_so_po/wizard/transfer_so_products.py b/addons/blanket_so_po/wizard/transfer_so_products.py
new file mode 100644
index 00000000..f76f211d
--- /dev/null
+++ b/addons/blanket_so_po/wizard/transfer_so_products.py
@@ -0,0 +1,52 @@
+# Part of Flectra. See LICENSE file for full copyright and licensing
+# details.
+
+from flectra import api, fields, models, _
+from flectra.exceptions import ValidationError
+
+
+class SaleTransferProducts(models.TransientModel):
+ _name = 'sale.transfer.products'
+ _description = 'Transfer products from Sale Lines'
+
+ transfer_qty = fields.Float("Transfer Qty")
+ ref_id = fields.Many2one('sale.order.line', string="Product Reference",
+ readonly=True)
+
+ @api.model
+ def default_get(self, fields):
+ context = dict(self._context)
+ result = super(SaleTransferProducts, self).default_get(fields)
+ sale_line = self.env['sale.order.line'].browse(
+ context.get('active_id'))
+ result.update({
+ 'ref_id': sale_line.id or False,
+ 'transfer_qty': sale_line.remaining_to_so_transfer})
+ return result
+
+ @api.multi
+ def split_qty_wt_newline(self):
+ if self.ref_id.remaining_to_so_transfer < self.transfer_qty:
+ raise ValidationError(_(
+ 'Sorry, You can not transfer more than requested or '
+ 'remains!'))
+ elif self.transfer_qty <= 0:
+ raise ValidationError(_(
+ 'Sorry, You can not transfer zero or negative '
+ 'quantity!'))
+ self.ref_id._action_launch_procurement_rule()
+ pickings = self.ref_id.order_id.picking_ids.filtered(
+ lambda p: p.state not in ('done', 'cancel'))
+ self.ref_id.remaining_to_so_transfer -= self.transfer_qty
+ for picking in pickings:
+ for line in picking.move_lines.filtered(
+ lambda l: l.product_id.id == self.ref_id.product_id.id and
+ l.sale_line_id.id == self.ref_id.id):
+ total_product_qty = self.ref_id.remaining_to_so_transfer \
+ + self.ref_id.qty_delivered
+ line.product_uom_qty = self.ref_id.product_uom_qty - \
+ total_product_qty
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
diff --git a/addons/blanket_so_po/wizard/transfer_so_products_view.xml b/addons/blanket_so_po/wizard/transfer_so_products_view.xml
new file mode 100755
index 00000000..7fa2e212
--- /dev/null
+++ b/addons/blanket_so_po/wizard/transfer_so_products_view.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ sale.transfer.products.form.view
+ sale.transfer.products
+
+
+
+
+
+
+ Transfer Products
+ sale.transfer.products
+ form
+ form
+
+ new
+
+
+
diff --git a/addons/purchase/tests/test_stockvaluation.py b/addons/purchase/tests/test_stockvaluation.py
index 2a39e208..93c8c83b 100644
--- a/addons/purchase/tests/test_stockvaluation.py
+++ b/addons/purchase/tests/test_stockvaluation.py
@@ -195,7 +195,7 @@ class TestStockValuation(TransactionCase):
wizard.process()
# the unit price of the stock move has been updated to the latest value
- self.assertEquals(move1.price_unit, price_unit_usd_new_rate)
+ self.assertEquals(round(move1.price_unit), round(price_unit_usd_new_rate))
self.assertAlmostEqual(self.product1.stock_value, price_unit_usd_new_rate * 10, delta=0.1)