diff --git a/compute_purchase_order/__init__.py b/compute_purchase_order/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/compute_purchase_order/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/compute_purchase_order/__openerp__.py b/compute_purchase_order/__openerp__.py
new file mode 100644
index 0000000..1018692
--- /dev/null
+++ b/compute_purchase_order/__openerp__.py
@@ -0,0 +1,21 @@
+# -*- encoding: utf-8 -*-
+{
+ 'name': 'Computed Purchase Order',
+ 'version': '9.0.1',
+ 'category': 'Purchase Order',
+ 'description': """ todo """,
+ 'author': 'Coop IT Easy',
+ 'website': 'https://github.com/coopiteasy/procurement-addons',
+ 'license': 'AGPL-3',
+ 'depends': [
+ 'product',
+ 'purchase',
+ 'stock',
+ 'stock_coverage',
+ ],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/computed_purchase_order.xml',
+ 'views/purchase_order.xml',
+ ],
+}
diff --git a/compute_purchase_order/models/__init__.py b/compute_purchase_order/models/__init__.py
new file mode 100644
index 0000000..6fdf87d
--- /dev/null
+++ b/compute_purchase_order/models/__init__.py
@@ -0,0 +1,4 @@
+from . import purchase_order
+from . import computed_purchase_order
+from . import computed_purchase_order_line
+from . import product_template
diff --git a/compute_purchase_order/models/computed_purchase_order.py b/compute_purchase_order/models/computed_purchase_order.py
new file mode 100644
index 0000000..2c4bb62
--- /dev/null
+++ b/compute_purchase_order/models/computed_purchase_order.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+from openerp import models, fields, api
+from openerp.exceptions import ValidationError
+
+
+class ComputedPurchaseOrder(models.Model):
+ _description = 'Computed Purchase Order'
+ _name = 'computed.purchase.order'
+ _order = 'id desc'
+
+ name = fields.Char(
+ string='CPO Reference',
+ size=64,
+ default='New')
+
+ order_date = fields.Datetime(
+ string='Purchase Order Date',
+ default=fields.Datetime.now,
+ help="Depicts the date where the Quotation should be validated and converted into a purchase order.") # noqa
+
+ date_planned = fields.Datetime(
+ string='Date Planned'
+ )
+
+ supplier_id = fields.Many2one(
+ 'res.partner',
+ string='Supplier',
+ readonly=True,
+ help="Supplier of the purchase order.")
+
+ order_line_ids = fields.One2many(
+ 'computed.purchase.order.line',
+ 'computed_purchase_order_id',
+ string='Order Lines',
+ )
+
+ total_amount = fields.Float(
+ string='Total Amount (w/o VAT)',
+ compute='_compute_cpo_total'
+ )
+
+ generated_purchase_order_ids = fields.One2many(
+ 'purchase.order',
+ 'original_cpo_id',
+ string='Generated Purchase Orders',
+ )
+
+ generated_po_count = fields.Integer(
+ string='Generated Purchase Order count',
+ compute='_compute_generated_po_count',
+ )
+
+ @api.model
+ def default_get(self, fields_list):
+ record = super(ComputedPurchaseOrder, self).default_get(fields_list)
+
+ record['date_planned'] = self._get_default_date_planned()
+ record['supplier_id'] = self._get_selected_supplier_id()
+ record['order_line_ids'] = self._create_order_lines()
+ record['name'] = self._compute_default_name()
+
+ return record
+
+ def _get_default_date_planned(self):
+ return fields.Datetime.now()
+
+ def _get_selected_supplier_id(self):
+ """
+ Calcule le vendeur associé qui a la date de début la plus récente et
+ plus petite qu’aujourd’hui pour chaque article sélectionné.
+ Will raise an error if more than two sellers are set
+ """
+ if 'active_ids' not in self.env.context:
+ return False
+
+ product_ids = self.env.context['active_ids']
+ products = self.env['product.template'].browse(product_ids)
+
+ suppliers = set()
+ for product in products:
+ main_supplier_id = product.main_supplier_id.id
+ suppliers.add(main_supplier_id)
+
+ if len(suppliers) == 0:
+ raise ValidationError(u'No supplier is set for selected articles.')
+ elif len(suppliers) == 1:
+ return suppliers.pop()
+ else:
+ raise ValidationError(
+ u'You must select article from a single supplier.')
+
+ def _create_order_lines(self):
+ product_tmpl_ids = self._get_selected_products()
+ cpol_ids = []
+ OrderLine = self.env['computed.purchase.order.line']
+ for product_id in product_tmpl_ids:
+ cpol = OrderLine.create(
+ {'computed_purchase_order_id': self.id,
+ 'product_template_id': product_id,
+ }
+ )
+ # should ideally be set in cpol defaults
+ cpol.purchase_quantity = cpol.minimum_purchase_qty
+ cpol_ids.append(cpol.id)
+ return cpol_ids
+
+ def _compute_default_name(self):
+ supplier_id = self._get_selected_supplier_id()
+ if supplier_id:
+ supplier_name = (
+ self.env['res.partner']
+ .browse(supplier_id)
+ .name)
+
+ name = u'CPO {} {}'.format(
+ supplier_name,
+ fields.Date.today())
+ else:
+ name = 'New'
+ return name
+
+ def _get_selected_products(self):
+ if 'active_ids' in self.env.context:
+ return self.env.context['active_ids']
+ else:
+ return []
+
+ @api.multi
+ def _compute_generated_po_count(self):
+ for cpo in self:
+ cpo.generated_po_count = len(cpo.generated_purchase_order_ids)
+
+ @api.multi
+ def get_generated_po_action(self):
+ self.ensure_one()
+ action = {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'purchase.order',
+ 'view_mode': 'tree,form,kanban',
+ 'target': 'current',
+ 'domain': [('id', 'in', self.generated_purchase_order_ids.ids)],
+ }
+ return action
+
+ # @api.onchange(order_line_ids) # fixme
+ @api.multi
+ def _compute_cpo_total(self):
+ for cpo in self:
+ total_amount = sum(cpol.subtotal for cpol in cpo.order_line_ids)
+ cpo.total_amount = total_amount
+
+ @api.multi
+ def create_purchase_order(self):
+ self.ensure_one()
+
+ if sum(self.order_line_ids.mapped('purchase_quantity')) == 0:
+ raise ValidationError(u'You need at least a product to generate '
+ u'a Purchase Order')
+
+ PurchaseOrder = self.env['purchase.order']
+ PurchaseOrderLine = self.env['purchase.order.line']
+
+ po_values = {
+ 'name': 'New',
+ 'date_order': self.order_date,
+ 'partner_id': self.supplier_id.id,
+ 'date_planned': self.date_planned,
+ }
+ purchase_order = PurchaseOrder.create(po_values)
+
+ for cpo_line in self.order_line_ids:
+ if cpo_line.purchase_quantity > 0:
+ pol_values = {
+ 'name': cpo_line.name,
+ 'product_id': cpo_line.get_default_product_product().id,
+ 'product_qty': cpo_line.purchase_quantity,
+ 'price_unit': cpo_line.product_price,
+ 'product_uom': cpo_line.uom_po_id.id,
+ 'order_id': purchase_order.id,
+ 'date_planned': self.date_planned,
+ }
+ PurchaseOrderLine.create(pol_values)
+
+ self.generated_purchase_order_ids += purchase_order
+
+ action = {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'purchase.order',
+ 'res_id': purchase_order.id,
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
+ return action
diff --git a/compute_purchase_order/models/computed_purchase_order_line.py b/compute_purchase_order/models/computed_purchase_order_line.py
new file mode 100644
index 0000000..92ca505
--- /dev/null
+++ b/compute_purchase_order/models/computed_purchase_order_line.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from openerp import models, fields, api
+from openerp.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+class ComputedPurchaseOrderLine(models.Model):
+ _description = 'Computed Purchase Order Line'
+ _name = 'computed.purchase.order.line'
+
+ computed_purchase_order_id = fields.Many2one(
+ 'computed.purchase.order',
+ string='Computed Purchase Order',
+ )
+
+ product_template_id = fields.Many2one(
+ 'product.template',
+ string='Linked Product Template',
+ required=True,
+ help='Product')
+
+ name = fields.Char(
+ string='Product Name',
+ related='product_template_id.name',
+ read_only=True)
+
+ supplierinfo_id = fields.Many2one(
+ 'product.supplierinfo',
+ string='Supplier information',
+ compute='_compute_supplierinfo',
+ store=True,
+ readonly=True,
+ )
+
+ category_id = fields.Many2one(
+ 'product.category',
+ string='Internal Category',
+ related='product_template_id.categ_id',
+ read_only=True)
+
+ uom_id = fields.Many2one(
+ 'product.uom',
+ string='Unit of Measure',
+ read_only=True,
+ related='product_template_id.uom_id',
+ help="Default Unit of Measure used for all stock operation.")
+
+ qty_available = fields.Float(
+ string='Stock Quantity',
+ related='product_template_id.qty_available',
+ read_only=True,
+ help='Quantity currently in stock. Does not take '
+ 'into account incoming orders.')
+
+ virtual_available = fields.Float(
+ string='Stock Quantity',
+ related='product_template_id.virtual_available',
+ read_only=True,
+ help='Virtual quantity taking into account current stock, incoming '
+ 'orders and outgoing sales.')
+
+ average_consumption = fields.Float(
+ string='Average Consumption',
+ related='product_template_id.average_consumption',
+ read_only=True)
+
+ stock_coverage = fields.Float(
+ string='Stock Coverage',
+ related='product_template_id.estimated_stock_coverage',
+ read_only=True,
+ )
+
+ minimum_purchase_qty = fields.Float(
+ string='Minimum Purchase Quantity',
+ compute='_depends_on_product_template',
+ )
+
+ purchase_quantity = fields.Float(
+ string='Purchase Quantity',
+ required=True,
+ default=0.)
+
+ uom_po_id = fields.Many2one(
+ 'product.uom',
+ string='Purchase Unit of Measure',
+ read_only=True,
+ related='product_template_id.uom_po_id',
+ help="Default Unit of Measure used for all stock operation.")
+
+ product_price = fields.Float(
+ string='Product Price (w/o VAT)',
+ compute='_depends_on_product_template',
+ read_only=True,
+ help='Supplier Product Price by buying unit. Price is without VAT')
+
+ virtual_coverage = fields.Float(
+ string='Expected Stock Coverage',
+ compute='_depends_on_purchase_quantity',
+ help='Expected stock coverage (in days) based on current stocks and average daily consumption') # noqa
+
+ subtotal = fields.Float(
+ string='Subtotal (w/o VAT)',
+ compute='_depends_on_purchase_quantity')
+
+ @api.multi
+ @api.depends('product_template_id')
+ def _depends_on_product_template(self):
+ for cpol in self:
+ # get supplier info
+ cpol.minimum_purchase_qty = cpol.supplierinfo_id.min_qty
+ cpol.product_price = cpol.supplierinfo_id.price
+
+ @api.multi
+ @api.onchange('product_template_id')
+ def _onchange_purchase_quantity(self):
+ for cpol in self:
+ cpol.purchase_quantity = cpol.supplierinfo_id.min_qty
+
+ @api.depends('purchase_quantity')
+ @api.multi
+ def _depends_on_purchase_quantity(self):
+ for cpol in self:
+ cpol.subtotal = cpol.product_price * cpol.purchase_quantity
+ avg = cpol.average_consumption
+ if avg > 0:
+ qty = cpol.virtual_available + cpol.purchase_quantity
+ cpol.virtual_coverage = qty / avg
+ else:
+ # todo what would be a good default value? (not float(inf))
+ cpol.virtual_coverage = 9999
+
+ return True
+
+ @api.multi
+ @api.depends('product_template_id')
+ @api.onchange('product_template_id')
+ def _compute_supplierinfo(self):
+ for cpol in self:
+ if not cpol.product_template_id:
+ cpol.supplierinfo_id = False
+ else:
+ SupplierInfo = self.env['product.supplierinfo']
+ si = SupplierInfo.search([
+ ('product_tmpl_id', '=', cpol.product_template_id.id),
+ ('name', '=', cpol.product_template_id.main_supplier_id.id) # noqa
+ ])
+
+ if len(si) == 0:
+ raise ValidationError(
+ u'No supplier information set for {name}'
+ .format(name=cpol.product_template_id.name))
+ elif len(si) == 1:
+ cpol.supplierinfo_id = si
+ else:
+ _logger.warning(
+ u'product {name} has several suppliers, chose last'
+ .format(name=cpol.product_template_id.name)
+ )
+ si = si.sorted(key=lambda r: r.create_date, reverse=True)
+ cpol.supplierinfo_id = si[0]
+
+ @api.constrains('purchase_quantity')
+ def _check_minimum_purchase_quantity(self):
+ for cpol in self:
+ if cpol.purchase_quantity < 0:
+ raise ValidationError(u'Purchase quantity for {product_name} must be greater than 0' # noqa
+ .format(product_name=cpol.product_template_id.name))
+ elif 0 < cpol.purchase_quantity < cpol.minimum_purchase_qty:
+ raise ValidationError(u'Purchase quantity for {product_name} must be greater than {min_qty}' # noqa
+ .format(product_name=cpol.product_template_id.name,
+ min_qty=cpol.minimum_purchase_qty))
+
+ @api.multi
+ def get_default_product_product(self):
+ self.ensure_one()
+ ProductProduct = self.env['product.product']
+ products = ProductProduct.search([
+ ('product_tmpl_id', '=', self.product_template_id.id)
+ ])
+
+ products = products.sorted(
+ key=lambda product: product.create_date,
+ reverse=True
+ )
+
+ if products:
+ return products[0]
+ else:
+ raise ValidationError(
+ u'%s:%s template has no variant set'
+ % (self.product_template_id.id, self.product_template_id.name)
+ )
+
diff --git a/compute_purchase_order/models/product_template.py b/compute_purchase_order/models/product_template.py
new file mode 100644
index 0000000..030719b
--- /dev/null
+++ b/compute_purchase_order/models/product_template.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from openerp import models, fields, api
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ main_supplier_id = fields.Many2one(
+ 'res.partner',
+ compute='_compute_main_supplier_id',
+ store=True
+ )
+
+ def _get_sorted_supplierinfo(self):
+ return self.seller_ids.sorted(
+ key=lambda seller: seller.date_start,
+ reverse=True)
+
+ @api.multi
+ @api.depends('seller_ids', 'seller_ids.date_start')
+ def _compute_main_supplier_id(self):
+ # Calcule le vendeur associé qui a la date de début la plus récente
+ # et plus petite qu’aujourd’hui
+ for pt in self:
+ sellers_ids = pt._get_sorted_supplierinfo()
+ pt.main_supplier_id = sellers_ids and sellers_ids[0].name or False
diff --git a/compute_purchase_order/models/purchase_order.py b/compute_purchase_order/models/purchase_order.py
new file mode 100644
index 0000000..24da3c6
--- /dev/null
+++ b/compute_purchase_order/models/purchase_order.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+from openerp import models, fields
+
+
+class PurchaseOrder(models.Model):
+ _inherit = "purchase.order"
+
+ original_cpo_id = fields.Many2one(
+ 'computed.purchase.order',
+ string='Original CPO',
+ help='CPO used to generate this Purchase Order'
+ )
diff --git a/compute_purchase_order/security/ir.model.access.csv b/compute_purchase_order/security/ir.model.access.csv
new file mode 100644
index 0000000..faf22c8
--- /dev/null
+++ b/compute_purchase_order/security/ir.model.access.csv
@@ -0,0 +1,12 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+
+access_purchase_order,purchase.order,model_computed_purchase_order,purchase.group_purchase_user,1,1,1,1
+access_purchase_order_manager,purchase.order,model_computed_purchase_order,purchase.group_purchase_manager,1,1,1,1
+access_purchase_order_stock_worker,purchase.order,model_computed_purchase_order,stock.group_stock_user,1,0,0,0
+access_purchase_order_invoicing_payments,purchase.order,model_computed_purchase_order,account.group_account_invoice,1,1,0,0
+
+access_purchase_order_line,purchase.order.line user,model_computed_purchase_order_line,purchase.group_purchase_user,1,1,1,1
+access_purchase_order_line_manager,purchase.order.line manager,model_computed_purchase_order_line,purchase.group_purchase_manager,1,0,0,0
+access_purchase_order_line_stock_worker,purchase.order.line,model_computed_purchase_order_line,stock.group_stock_user,1,0,0,0
+access_purchase_order_line_manager,purchase.order.line,model_computed_purchase_order_line,purchase.group_purchase_manager,1,1,1,1
+access_purchase_order_line_invoicing_payments,purchase.order.line,model_computed_purchase_order_line,account.group_account_invoice,1,1,0,0
diff --git a/compute_purchase_order/views/computed_purchase_order.xml b/compute_purchase_order/views/computed_purchase_order.xml
new file mode 100644
index 0000000..10038e1
--- /dev/null
+++ b/compute_purchase_order/views/computed_purchase_order.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+ computed.purchase.order.tree
+ computed.purchase.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+ computed.purchase.order.form
+ computed.purchase.order
+
+
+
+
+
+
+
+ computed.purchase.order
+
+
+
+
+
+
+
+
+
+
+ Computed Purchase Orders
+ ir.actions.act_window
+ computed.purchase.order
+ tree,form
+
+
+
+
+
+
+
+
+
+
diff --git a/compute_purchase_order/views/product_template.xml b/compute_purchase_order/views/product_template.xml
new file mode 100644
index 0000000..c5607c8
--- /dev/null
+++ b/compute_purchase_order/views/product_template.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ product.template.tree
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/compute_purchase_order/views/purchase_order.xml b/compute_purchase_order/views/purchase_order.xml
new file mode 100644
index 0000000..75dc354
--- /dev/null
+++ b/compute_purchase_order/views/purchase_order.xml
@@ -0,0 +1,13 @@
+
+
+
+ purchase.order.form.inherit
+ purchase.order
+
+
+
+
+
+
+
+