robin.keunen
5 years ago
8 changed files with 489 additions and 441 deletions
-
7compute_purchase_order/__manifest__.py
-
299compute_purchase_order/models/computed_purchase_order.py
-
242compute_purchase_order/models/computed_purchase_order_line.py
-
26compute_purchase_order/models/product_template.py
-
42compute_purchase_order/models/purchase_order.py
-
1compute_purchase_order/tests/__init__.py
-
84compute_purchase_order/tests/test_cpo.py
-
33compute_purchase_order/views/computed_purchase_order.xml
@ -1,228 +1,167 @@ |
|||||
from odoo import models, fields, api, SUPERUSER_ID |
|
||||
|
# Copyright 2020 Coop IT Easy SCRL fs |
||||
|
# Robin Keunen <robin@coopiteasy.be> |
||||
|
# Vincent Van Rossem <vincent@coopiteasy.be> |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from odoo import models, fields, api, _ |
||||
from odoo.exceptions import ValidationError |
from odoo.exceptions import ValidationError |
||||
|
|
||||
|
|
||||
class ComputedPurchaseOrder(models.Model): |
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') |
|
||||
|
_description = "Computed Purchase Order" |
||||
|
_name = "computed.purchase.order" |
||||
|
_order = "id desc" |
||||
|
|
||||
|
name = fields.Char(string="CPO Reference", default=_("New")) |
||||
order_date = fields.Datetime( |
order_date = fields.Datetime( |
||||
string='Purchase Order Date', |
|
||||
|
string="Purchase Order Date", |
||||
default=fields.Datetime.now, |
default=fields.Datetime.now, |
||||
help="Depicts the date where the Quotation should be validated and converted into a purchase order.") # noqa |
|
||||
|
|
||||
|
help="Date at which the Quotation should be validated and " |
||||
|
"converted into a purchase order.", |
||||
|
) |
||||
date_planned = fields.Datetime( |
date_planned = fields.Datetime( |
||||
string='Date Planned' |
|
||||
|
string="Date Planned", |
||||
|
default=fields.Datetime.now, # default=lambda _: fields.Datetime.now() |
||||
) |
) |
||||
|
|
||||
supplier_id = fields.Many2one( |
supplier_id = fields.Many2one( |
||||
'res.partner', |
|
||||
string='Supplier', |
|
||||
|
comodel_name="res.partner", |
||||
|
string="Supplier", |
||||
readonly=True, |
readonly=True, |
||||
help="Supplier of the purchase order.") |
|
||||
|
|
||||
order_line_ids = fields.One2many( |
|
||||
'computed.purchase.order.line', |
|
||||
'computed_purchase_order_id', |
|
||||
string='Order Lines', |
|
||||
|
help="Supplier of the purchase order.", |
||||
|
) |
||||
|
cpo_line_ids = fields.One2many( |
||||
|
comodel_name="computed.purchase.order.line", |
||||
|
inverse_name="cpo_id", |
||||
|
string="Order Lines", |
||||
) |
) |
||||
|
|
||||
total_amount = fields.Float( |
total_amount = fields.Float( |
||||
string='Total Amount (w/o VAT)', |
|
||||
compute='_compute_cpo_total', |
|
||||
store=True, |
|
||||
|
string="Total Amount (w/o VAT)", compute="_compute_cpo_total" |
||||
) |
) |
||||
|
|
||||
generated_purchase_order_ids = fields.One2many( |
generated_purchase_order_ids = fields.One2many( |
||||
'purchase.order', |
|
||||
'original_cpo_id', |
|
||||
string='Generated Purchase Orders', |
|
||||
|
comodel_name="purchase.order", |
||||
|
inverse_name="original_cpo_id", |
||||
|
string="Generated Purchase Orders", |
||||
) |
) |
||||
|
|
||||
generated_po_count = fields.Integer( |
generated_po_count = fields.Integer( |
||||
string='Generated Purchase Order count', |
|
||||
compute='_compute_generated_po_count', |
|
||||
|
string="Generated Purchase Order count", |
||||
|
compute="_compute_generated_po_count", |
||||
) |
) |
||||
|
|
||||
|
@api.multi |
||||
|
@api.depends("cpo_line_ids", "cpo_line_ids.purchase_quantity") |
||||
|
def _compute_cpo_total(self): |
||||
|
for cpo in self: |
||||
|
total_amount = sum(cpol.subtotal for cpol in cpo.cpo_line_ids) |
||||
|
cpo.total_amount = total_amount |
||||
|
|
||||
@api.model |
@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('No supplier is set for selected articles.') |
|
||||
|
def _get_selected_supplier(self): |
||||
|
product_ids = self.env.context.get("active_ids", []) |
||||
|
products = self.env["product.template"].browse(product_ids) |
||||
|
suppliers = products.mapped("main_supplier_id") |
||||
|
|
||||
|
if not suppliers: |
||||
|
raise ValidationError("No supplier is set for selected articles.") |
||||
elif len(suppliers) == 1: |
elif len(suppliers) == 1: |
||||
return suppliers.pop() |
|
||||
|
return suppliers |
||||
else: |
else: |
||||
raise ValidationError( |
raise ValidationError( |
||||
'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, |
|
||||
} |
|
||||
|
"You must select article from a single supplier." |
||||
) |
) |
||||
# 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 = '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() |
|
||||
|
@api.model |
||||
|
def generate_cpo(self): |
||||
|
order_line_obj = self.env["computed.purchase.order.line"] |
||||
|
product_ids = self.env.context.get("active_ids", []) |
||||
|
|
||||
|
supplier = self._get_selected_supplier() |
||||
|
name = "CPO {} {}".format(supplier.name, fields.Date.today()) |
||||
|
cpo = self.create({"name": name, "supplier_id": supplier.id}) |
||||
|
|
||||
|
for product_id in product_ids: |
||||
|
supplierinfo = self.env["product.supplierinfo"].search( |
||||
|
[ |
||||
|
("product_tmpl_id", "=", product_id), |
||||
|
("name", "=", supplier.id), |
||||
|
] |
||||
|
) |
||||
|
min_qty = supplierinfo.min_qty if supplierinfo else 0 |
||||
|
order_line_obj.create( |
||||
|
{ |
||||
|
"cpo_id": cpo.id, |
||||
|
"product_template_id": product_id, |
||||
|
"purchase_quantity": min_qty, |
||||
|
} |
||||
|
) |
||||
action = { |
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)], |
|
||||
|
"type": "ir.actions.act_window", |
||||
|
"res_model": "computed.purchase.order", |
||||
|
"views": [[False, "form"]], |
||||
|
# "view_mode": "form,tree", |
||||
|
# "view_type": "form", |
||||
|
# "target": "current", |
||||
|
"res_id": cpo.id, |
||||
} |
} |
||||
return action |
return action |
||||
|
|
||||
@api.depends('order_line_ids.subtotal') |
|
||||
@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 |
@api.multi |
||||
def create_purchase_order(self): |
def create_purchase_order(self): |
||||
self.ensure_one() |
self.ensure_one() |
||||
|
|
||||
if sum(self.order_line_ids.mapped('purchase_quantity')) == 0: |
|
||||
raise ValidationError('You need at least a product to generate ' |
|
||||
'a Purchase Order') |
|
||||
|
|
||||
PurchaseOrder = self.env['purchase.order'] |
|
||||
PurchaseOrderLine = self.env['purchase.order.line'] |
|
||||
|
if sum(self.cpo_line_ids.mapped("purchase_quantity")) == 0: |
||||
|
raise ValidationError( |
||||
|
"You need at least a product to generate " "a Purchase Order" |
||||
|
) |
||||
|
|
||||
po_values = { |
|
||||
'name': 'New', |
|
||||
'date_order': self.order_date, |
|
||||
'partner_id': self.supplier_id.id, |
|
||||
'date_planned': self.date_planned, |
|
||||
|
purchase_order = self.env["purchase.order"].create( |
||||
|
{ |
||||
|
"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: |
|
||||
|
for cpo_line in self.cpo_line_ids: |
||||
if cpo_line.purchase_quantity > 0: |
if cpo_line.purchase_quantity > 0: |
||||
if cpo_line.supplierinfo_id.product_code: |
|
||||
pol_name = '[%s] %s' % (cpo_line.supplierinfo_id.product_code, cpo_line.name) |
|
||||
else: |
|
||||
pol_name = cpo_line.name |
|
||||
pol_values = { |
|
||||
'name': pol_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, |
|
||||
|
pol = self.env["purchase.order.line"].create( |
||||
|
{ |
||||
|
"name": cpo_line.name, |
||||
|
"product_id": cpo_line.product_template_id.product_variant_id.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, |
||||
} |
} |
||||
pol = PurchaseOrderLine.create(pol_values) |
|
||||
|
) |
||||
pol.compute_taxes_id() |
pol.compute_taxes_id() |
||||
|
|
||||
self.generated_purchase_order_ids += purchase_order |
self.generated_purchase_order_ids += purchase_order |
||||
|
|
||||
action = { |
action = { |
||||
'type': 'ir.actions.act_window', |
|
||||
'res_model': 'purchase.order', |
|
||||
'res_id': purchase_order.id, |
|
||||
'view_type': 'form', |
|
||||
'view_mode': 'form', |
|
||||
'target': 'current', |
|
||||
|
"type": "ir.actions.act_window", |
||||
|
"res_model": "purchase.order", |
||||
|
"res_id": purchase_order.id, |
||||
|
"view_type": "form", |
||||
|
"view_mode": "form,tree", |
||||
|
"target": "current", |
||||
} |
} |
||||
return action |
return action |
||||
|
|
||||
|
|
||||
class PurchaseOrderLine(models.Model): |
|
||||
_inherit = 'purchase.order.line' |
|
||||
|
|
||||
@api.multi |
@api.multi |
||||
def compute_taxes_id(self): |
|
||||
for pol in self: |
|
||||
if self.env.uid == SUPERUSER_ID: |
|
||||
company_id = self.env.user.company_id.id |
|
||||
else: |
|
||||
company_id = self.company_id.id |
|
||||
|
|
||||
fpos_id = ( |
|
||||
self.env['account.fiscal.position'] |
|
||||
.with_context(company_id=company_id) |
|
||||
.get_fiscal_position(pol.partner_id.id) |
|
||||
) |
|
||||
fpos = self.env['account.fiscal.position'].browse(fpos_id) |
|
||||
pol.order_id.fiscal_position_id = fpos |
|
||||
|
|
||||
taxes = self.product_id.supplier_taxes_id |
|
||||
taxes_id = fpos.map_tax(taxes) if fpos else taxes |
|
||||
|
|
||||
if taxes_id: |
|
||||
taxes_id = taxes_id.filtered( |
|
||||
lambda t: t.company_id.id == company_id) |
|
||||
|
@api.depends("generated_purchase_order_ids") |
||||
|
def _compute_generated_po_count(self): |
||||
|
for cpo in self: |
||||
|
cpo.generated_po_count = len(cpo.generated_purchase_order_ids) |
||||
|
|
||||
pol.taxes_id = taxes_id |
|
||||
|
@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 |
@ -1,11 +1,45 @@ |
|||||
from odoo import models, fields |
|
||||
|
# Copyright 2020 Coop IT Easy SCRL fs |
||||
|
# Robin Keunen <robin@coopiteasy.be> |
||||
|
# Vincent Van Rossem <vincent@coopiteasy.be> |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
from odoo import models, fields, api, SUPERUSER_ID |
||||
|
|
||||
|
|
||||
class PurchaseOrder(models.Model): |
class PurchaseOrder(models.Model): |
||||
_inherit = "purchase.order" |
_inherit = "purchase.order" |
||||
|
|
||||
original_cpo_id = fields.Many2one( |
original_cpo_id = fields.Many2one( |
||||
'computed.purchase.order', |
|
||||
string='Original CPO', |
|
||||
help='CPO used to generate this Purchase Order' |
|
||||
|
"computed.purchase.order", |
||||
|
string="Original CPO", |
||||
|
help="CPO used to generate this Purchase Order", |
||||
) |
) |
||||
|
|
||||
|
|
||||
|
class PurchaseOrderLine(models.Model): |
||||
|
_inherit = "purchase.order.line" |
||||
|
|
||||
|
@api.multi |
||||
|
def compute_taxes_id(self): |
||||
|
for pol in self: |
||||
|
if self.env.uid == SUPERUSER_ID: |
||||
|
company_id = self.env.user.company_id.id |
||||
|
else: |
||||
|
company_id = self.company_id.id |
||||
|
|
||||
|
fpos_id = ( |
||||
|
self.env["account.fiscal.position"] |
||||
|
.with_context(company_id=company_id) |
||||
|
.get_fiscal_position(pol.partner_id.id) |
||||
|
) |
||||
|
fpos = self.env["account.fiscal.position"].browse(fpos_id) |
||||
|
pol.order_id.fiscal_position_id = fpos |
||||
|
|
||||
|
taxes = self.product_id.supplier_taxes_id |
||||
|
taxes_id = fpos.map_tax(taxes) if fpos else taxes |
||||
|
|
||||
|
if taxes_id: |
||||
|
taxes_id = taxes_id.filtered( |
||||
|
lambda t: t.company_id.id == company_id |
||||
|
) |
||||
|
|
||||
|
pol.taxes_id = taxes_id |
@ -0,0 +1 @@ |
|||||
|
from . import test_cpo |
@ -0,0 +1,84 @@ |
|||||
|
# Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) |
||||
|
# @author: Robin Keunen <robin@coopiteasy.be> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from odoo.tests.common import TransactionCase, Form |
||||
|
|
||||
|
|
||||
|
class TestCPO(TransactionCase): |
||||
|
def setUp(self): |
||||
|
super().setUp() |
||||
|
|
||||
|
self.supplier = self.browse_ref("base.res_partner_1") |
||||
|
self.pproduct1 = self.browse_ref("product.product_product_25") |
||||
|
self.ptemplate1 = self.pproduct1.product_tmpl_id |
||||
|
self.pproduct2 = self.browse_ref("product.product_delivery_02") |
||||
|
self.ptemplate2 = self.pproduct2.product_tmpl_id |
||||
|
|
||||
|
def test_generate_cpo(self): |
||||
|
supplierinfo_obj = self.env["product.supplierinfo"] |
||||
|
supplierinfo = supplierinfo_obj.search( |
||||
|
[ |
||||
|
("name", "=", self.supplier.id), |
||||
|
("product_tmpl_id", "=", self.ptemplate1.id), |
||||
|
] |
||||
|
) |
||||
|
supplierinfo2 = supplierinfo_obj.search( |
||||
|
[ |
||||
|
("name", "=", self.supplier.id), |
||||
|
("product_tmpl_id", "=", self.ptemplate2.id), |
||||
|
] |
||||
|
) |
||||
|
|
||||
|
cpo_obj = self.env["computed.purchase.order"] |
||||
|
cpo_action = cpo_obj.with_context( |
||||
|
active_ids=[self.ptemplate1.id] |
||||
|
).generate_cpo() |
||||
|
cpo = cpo_obj.browse(cpo_action["res_id"]) |
||||
|
cpol = cpo.cpo_line_ids # expect one line |
||||
|
|
||||
|
self.assertEquals(cpo.supplier_id, self.supplier) |
||||
|
self.assertEquals(cpol.product_template_id, self.ptemplate1) |
||||
|
self.assertEquals(cpol.product_price, supplierinfo.price) |
||||
|
self.assertEquals(cpol.purchase_quantity, supplierinfo.min_qty) |
||||
|
|
||||
|
# testing triggers |
||||
|
expected_subtotal = supplierinfo.price * supplierinfo.min_qty |
||||
|
self.assertEquals(cpol.subtotal, expected_subtotal) |
||||
|
|
||||
|
cpol.purchase_quantity = 4 |
||||
|
expected_subtotal = supplierinfo.price * 4 |
||||
|
self.assertEquals(cpol.subtotal, expected_subtotal) |
||||
|
|
||||
|
cpo_form = Form(cpo) |
||||
|
with cpo_form.cpo_line_ids.edit(index=0) as line_form: |
||||
|
line_form.product_template_id = self.ptemplate2 |
||||
|
self.assertEquals(line_form.product_template_id, self.ptemplate2) |
||||
|
cpo = cpo_form.save() |
||||
|
cpol = cpo.cpo_line_ids |
||||
|
|
||||
|
expected_subtotal = supplierinfo2.price * supplierinfo2.min_qty |
||||
|
self.assertEquals(cpol.product_price, supplierinfo2.price) |
||||
|
self.assertEquals(cpol.purchase_quantity, supplierinfo2.min_qty) |
||||
|
self.assertEquals(cpol.subtotal, expected_subtotal) |
||||
|
|
||||
|
def test_generate_po(self): |
||||
|
cpo_obj = self.env["computed.purchase.order"] |
||||
|
cpo_action = cpo_obj.with_context( |
||||
|
active_ids=[self.ptemplate1.id, self.ptemplate2.id] |
||||
|
).generate_cpo() |
||||
|
cpo = cpo_obj.browse(cpo_action["res_id"]) |
||||
|
po_action = cpo.create_purchase_order() |
||||
|
po = self.env["purchase.order"].browse(po_action["res_id"]) |
||||
|
|
||||
|
self.assertEquals(cpo.supplier_id, po.partner_id) |
||||
|
self.assertEquals(len(cpo.cpo_line_ids), len(po.order_line)) |
||||
|
lines = zip( |
||||
|
cpo.cpo_line_ids.sorted(lambda l: l.product_template_id), |
||||
|
po.order_line.sorted(lambda l: l.product_id.product_tmpl_id), |
||||
|
) |
||||
|
for cpol, pol in lines: |
||||
|
self.assertEquals( |
||||
|
cpol.product_template_id, pol.product_id.product_tmpl_id |
||||
|
) |
||||
|
self.assertEquals(cpol.purchase_quantity, pol.product_qty) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue