Skip to content

Commit

Permalink
Merge PR #803 into 13.0
Browse files Browse the repository at this point in the history
Signed-off-by simahawk
  • Loading branch information
shopinvader-git-bot committed May 21, 2021
2 parents e8bfc66 + 1909e2b commit 8c95d3b
Show file tree
Hide file tree
Showing 33 changed files with 1,565 additions and 11 deletions.
6 changes: 6 additions & 0 deletions setup/shopinvader_customer_price/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
6 changes: 6 additions & 0 deletions setup/shopinvader_customer_price_wishlist/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
4 changes: 2 additions & 2 deletions shopinvader/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def setUpClass(cls):
]
)
cls.env.user.company_id.currency_id = cls.env.ref("base.USD")
base_price_list = cls.env.ref("product.list0")
base_price_list.currency_id = cls.env.ref("base.USD")
cls.base_pricelist = cls.env.ref("product.list0")
cls.base_pricelist.currency_id = cls.env.ref("base.USD")
cls.shopinvader_variant.record_id.currency_id = cls.env.ref("base.USD")


Expand Down
16 changes: 7 additions & 9 deletions shopinvader/tests/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ def test_product_name_url(self):
return

def test_product_get_price(self):
# base_price_list doesn't define a tax mapping. We are tax included
base_price_list = self.env.ref("product.list0")
# self.base_pricelist doesn't define a tax mapping. We are tax included
fiscal_position_fr = self.env.ref("shopinvader.fiscal_position_0")
price = self.shopinvader_variant._get_price(
base_price_list, fiscal_position_fr
self.base_pricelist, fiscal_position_fr
)
self.assertDictEqual(
price,
Expand Down Expand Up @@ -134,7 +133,7 @@ def test_product_get_price(self):
"shopinvader.fiscal_position_1"
)
price = self.shopinvader_variant._get_price(
base_price_list, tax_exclude_fiscal_position
self.base_pricelist, tax_exclude_fiscal_position
)
self.assertDictEqual(
price,
Expand All @@ -161,13 +160,12 @@ def test_product_get_price(self):
def test_product_get_price_discount_policy(self):
# Ensure that discount is with 2 digits
self.env.ref("product.decimal_discount").digits = 2
# base_price_list doesn't define a tax mapping. We are tax included
# self.base_pricelist doesn't define a tax mapping. We are tax included
# we modify the discount_policy
base_price_list = self.env.ref("product.list0")
base_price_list.discount_policy = "without_discount"
self.base_pricelist.discount_policy = "without_discount"
fiscal_position_fr = self.env.ref("shopinvader.fiscal_position_0")
price = self.shopinvader_variant._get_price(
base_price_list, fiscal_position_fr
self.base_pricelist, fiscal_position_fr
)
self.assertDictEqual(
price,
Expand Down Expand Up @@ -202,7 +200,7 @@ def test_product_get_price_discount_policy(self):
"shopinvader.fiscal_position_1"
)
price = self.shopinvader_variant._get_price(
base_price_list, tax_exclude_fiscal_position
self.base_pricelist, tax_exclude_fiscal_position
)
self.assertDictEqual(
price,
Expand Down
111 changes: 111 additions & 0 deletions shopinvader_customer_price/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
==========================
Shopinvader Customer Price
==========================

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-shopinvader%2Fshopinvader-lightgray.png?logo=github
:target: https://github.com/shopinvader/shopinvader/tree/13.0/shopinvader_customer_price
:alt: shopinvader/shopinvader

|badge1| |badge2| |badge3|

Handle customer specific prices.

Provides:

* endpoint `/customer_price/products` to fetch customer prices for products.
* backend configuration to state which pricelist should be used by partner
(by selecting a partner field that relates to pricelists)


**Use case**

Thousands of customers and at least 1 pricelist per each customer.
You want to display customer specific prices in the frontend on demand.
For instance: product page, wishlists, etc.

**Rationale**

One of the key points of Shopinvader's speed
is the delegation of products' data indexing to external search engines.

While this is perfect for generic data and not so complex price rules,
if you have very special prices per each customer that's a blocker,
and you'd need to index all prices for all customers to make it work seemlessly.

**Warning**

It's strongly recommended to not call the endpoint for each product on search results
otherwise you'll get potentially thousands of requests to Odoo.

Also, when setting the pricelist field for the partner,
beware that prices in the indexes might differ from the prices in the cart.

**Table of contents**

.. contents::
:local:

Known issues / Roadmap
======================

Probably the best option would be to have 1 index per customer
which would even allow to sort and filter products by customer's prices
but this requires a lot of work with current implementation of search engine machinery.

If you use Algolia this is probably a no-go as it would cost too much.
In the context of ElasticSearch instead you could afford it.

Things that would be needed to go for an indexed solution:

* make language not required on indexes (at the momemt the whole SE machinery relies on languages)
* automatically generate one index per each pricelist/customer
* make the frontend capable of switching indexes depending on the customer

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/shopinvader/shopinvader/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/shopinvader/shopinvader/issues/new?body=module:%20shopinvader_customer_price%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Camptocamp

Contributors
~~~~~~~~~~~~

* Simone Orsi <[email protected]>

Other credits
~~~~~~~~~~~~~

The development of this module has been financially supported by:

* Camptocamp
* Cosanum

Maintainers
~~~~~~~~~~~

This module is part of the `shopinvader/shopinvader <https://github.com/shopinvader/shopinvader/tree/13.0/shopinvader_customer_price>`_ project on GitHub.

You are welcome to contribute.
2 changes: 2 additions & 0 deletions shopinvader_customer_price/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import services
from . import models
13 changes: 13 additions & 0 deletions shopinvader_customer_price/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Shopinvader Customer Price",
"summary": """Expose customer's specific prices.""",
"version": "13.0.1.0.0",
"license": "AGPL-3",
"author": "Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/shopinvader/odoo-shopinvader",
"depends": ["shopinvader"],
"data": ["views/shopinvader_backend_views.xml"],
}
1 change: 1 addition & 0 deletions shopinvader_customer_price/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import shopinvader_backend
63 changes: 63 additions & 0 deletions shopinvader_customer_price/models/shopinvader_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com).
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import fields, models, tools


class ShopinvaderBackend(models.Model):

_inherit = "shopinvader.backend"

cart_pricelist_partner_field_id = fields.Many2one(
comodel_name="ir.model.fields",
domain=[
("model", "=", "res.partner"),
("ttype", "=", "many2one"),
("relation", "=", "product.pricelist"),
],
help="Set the partner pricelist that will be used for the cart. "
"WARNING: by changing this you might have a mismatch "
"between the prices showed on the cart "
"and the ones showed on product details. "
"The default pricelist will still be used for products' indexes.",
)

@tools.ormcache("partner.id", "self.cart_pricelist_partner_field_id.id")
def _get_cart_pricelist_id(self, partner):
if self.cart_pricelist_partner_field_id:
pricelist = partner[self.cart_pricelist_partner_field_id.name]
return pricelist.id
return None

def _get_cart_pricelist(self, partner=None):
pricelist = super()._get_cart_pricelist(partner)
if partner:
pricelist_id = self._get_cart_pricelist_id(partner)
if pricelist_id:
return self.env["product.pricelist"].browse(pricelist_id)
return pricelist

def _get_partner_pricelist(self, partner):
pricelist = super()._get_partner_pricelist(partner)
if pricelist is None:
pricelist = partner.property_product_pricelist
return pricelist

@tools.ormcache("partner.id", "self.company_id.id")
def _get_fiscal_position_id(self, partner):
fp_model = self.env["account.fiscal.position"].with_context(
force_company=self.company_id.id
)
fpos_id = fp_model.get_fiscal_position(
partner.id, delivery_id=partner.id,
)
return fpos_id

def _get_fiscal_position(self, partner):
fpos_id = self._get_fiscal_position_id(partner)
return (
self.env["account.fiscal.position"].browse(fpos_id)
if fpos_id
else self.env["account.fiscal.position"].browse()
)
1 change: 1 addition & 0 deletions shopinvader_customer_price/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Simone Orsi <[email protected]>
4 changes: 4 additions & 0 deletions shopinvader_customer_price/readme/CREDITS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The development of this module has been financially supported by:

* Camptocamp
* Cosanum
31 changes: 31 additions & 0 deletions shopinvader_customer_price/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Handle customer specific prices.

Provides:

* endpoint `/customer_price/products` to fetch customer prices for products.
* backend configuration to state which pricelist should be used by partner
(by selecting a partner field that relates to pricelists)


**Use case**

Thousands of customers and at least 1 pricelist per each customer.
You want to display customer specific prices in the frontend on demand.
For instance: product page, wishlists, etc.

**Rationale**

One of the key points of Shopinvader's speed
is the delegation of products' data indexing to external search engines.

While this is perfect for generic data and not so complex price rules,
if you have very special prices per each customer that's a blocker,
and you'd need to index all prices for all customers to make it work seemlessly.

**Warning**

It's strongly recommended to not call the endpoint for each product on search results
otherwise you'll get potentially thousands of requests to Odoo.

Also, when setting the pricelist field for the partner,
beware that prices in the indexes might differ from the prices in the cart.
12 changes: 12 additions & 0 deletions shopinvader_customer_price/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Probably the best option would be to have 1 index per customer
which would even allow to sort and filter products by customer's prices
but this requires a lot of work with current implementation of search engine machinery.

If you use Algolia this is probably a no-go as it would cost too much.
In the context of ElasticSearch instead you could afford it.

Things that would be needed to go for an indexed solution:

* make language not required on indexes (at the momemt the whole SE machinery relies on languages)
* automatically generate one index per each pricelist/customer
* make the frontend capable of switching indexes depending on the customer
1 change: 1 addition & 0 deletions shopinvader_customer_price/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import customer_price
61 changes: 61 additions & 0 deletions shopinvader_customer_price/services/customer_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2020 Camptocamp (http://www.camptocamp.com).
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo.osv import expression

from odoo.addons.base_rest.components.service import to_int
from odoo.addons.component.core import Component


class CustomerPriceService(Component):
"""Shopinvader service to expose customer specific product prices.
"""

_name = "shopinvader.customer.price.service"
_inherit = "base.shopinvader.service"
_usage = "customer_price"
_expose_model = "shopinvader.variant"
_description = __doc__

def products(self, **params):
domain = expression.normalize_domain(self._get_base_search_domain())
domain = expression.AND([domain, [("id", "in", params["ids"])]])
records = self.env[self._expose_model].search(domain)
return self._to_json(records, one=params.get("one"))

def _validator_products(self):
return {
"ids": {
"type": "list",
"nullable": True,
"required": True,
"schema": {"coerce": to_int, "type": "integer"},
},
"one": {"type": "boolean", "nullable": True, "required": False},
}

def _get_base_search_domain(self):
if not self._is_logged_in():
return expression.FALSE_DOMAIN
return super()._get_base_search_domain()

def _to_json(self, records, **kw):
return records.jsonify(self._json_parser(), **kw)

def _json_parser(self):
return [
"id",
("record_id:objectID", lambda rec, fname: rec[fname].id),
("price", self._get_price),
]

def _get_price(self, record, fname):
pricelist = self.shopinvader_backend._get_cart_pricelist(self.partner)
fposition = self.shopinvader_backend._get_fiscal_position(self.partner)
company = self.shopinvader_backend.company_id
return {
self.invader_partner.role: record._get_price(
pricelist, fposition, company=company
)
}
Loading

0 comments on commit 8c95d3b

Please sign in to comment.