From 4679dd2bbefb90a3ee3b2199199af52523067e66 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 19 Nov 2016 16:38:37 +0100 Subject: [PATCH] PW-570 inquiry price calculations --- api/lib/prices.py | 48 ++++++++++++++++-- api/models/__init__.py | 2 +- api/models/inquiry.py | 45 ++++++++++++++++- api/models/product.py | 9 ++++ api/resources/prices.py | 91 ++++++++++++++++++++++++++++++++-- docs/prices.md | 83 +++++++++++++++++++++++++++++-- tests/config.py | 2 +- tests/lib/test_prices.py | 81 ++++++++++++++++++++++++++++++ tests/resources/test_prices.py | 25 +++++++++- 9 files changed, 371 insertions(+), 15 deletions(-) diff --git a/api/lib/prices.py b/api/lib/prices.py index d09a293..c551fb8 100644 --- a/api/lib/prices.py +++ b/api/lib/prices.py @@ -5,7 +5,7 @@ from api.lib.utils import round_half_up from api.lib.limits import calculate_canvas_limits -def m2_price(material, market, designer=None, reseller=None, rounded=True, inc_vat=True): +def m2_price(material, market, designer=None, reseller=None, inquiry=None, rounded=True, inc_vat=True): """ calculates price per square meter """ price = material.price @@ -15,6 +15,9 @@ def m2_price(material, market, designer=None, reseller=None, rounded=True, inc_v if reseller: price *= (float(reseller.pricepremium) / 100) + 1 + if inquiry: + price *= (float(inquiry.pricepremium) / 100) + 1 + price *= market.exchange_rate price *= market.price_adjustments @@ -27,9 +30,9 @@ def m2_price(material, market, designer=None, reseller=None, rounded=True, inc_v return price -def wallpaper_price(width, height, material, market, designer=None, reseller=None, rounded=True, inc_vat=True): +def wallpaper_price(width, height, material, market, designer=None, reseller=None, inquiry=None, rounded=True, inc_vat=True): price = m2_price(material, market, designer, reseller, - rounded=rounded, inc_vat=False) + inquiry, rounded=rounded, inc_vat=False) sqm = (width / 100) * (height / 100) @@ -150,10 +153,10 @@ def old_canvas_diy_frame_price(width, height, market, reseller=None, rounded=Tru return canvas_frame_price(width, height, stock_price, market, additional_price, reseller=reseller, rounded=rounded, inc_vat=inc_vat) -def old_canvas_price(width, height, material, market, framed=True, designer=None, reseller=None, inc_vat=True): +def old_canvas_price(width, height, material, market, framed=True, designer=None, reseller=None, inquiry=None, inc_vat=True): width, height = calculate_canvas_limits(width, height, framed) price = m2_price(material, market, designer, reseller, - rounded=False, inc_vat=False) + inquiry, rounded=False, inc_vat=False) # calculate square meter sqm = (width / 100) * (height / 100) @@ -173,3 +176,38 @@ def old_canvas_price(width, height, material, market, framed=True, designer=None price *= market.vat return round_half_up(price) + + +def inquiry_price(inquiry, inc_price_retouch=True, rounded=True, inc_vat=True, **kwargs): + width = kwargs.get('width', inquiry.width) + height = kwargs.get('height', inquiry.height) + material = kwargs.get('material', inquiry.material) + framed = kwargs.get('framed', inquiry.framed) + + if inquiry.product_group == 'photo-wallpaper': + price = wallpaper_price(width, height, material, inquiry.market, + designer=inquiry.designer, reseller=inquiry.dealer_store, inquiry=inquiry, rounded=False, inc_vat=False) + elif inquiry.product_group == 'canvas': + price = old_canvas_price(width, height, material, inquiry.market, + designer=inquiry.designer, reseller=inquiry.dealer_store, inquiry=inquiry, framed=framed, inc_vat=False) + else: + raise ValueError( + 'Invalid product group: {}'.format(inquiry.product_group)) + + effect_price = inquiry.price_effect + if effect_price: + price += float(effect_price) + + if inc_price_retouch: + retouch_price = inquiry.local_price_retouch( + rounded=False, inc_vat=False) + if retouch_price: + price += retouch_price + + if inc_vat: + price *= inquiry.market.vat + + if rounded: + price = round_half_up(price) + + return price diff --git a/api/models/__init__.py b/api/models/__init__.py index 0527000..e005499 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -4,4 +4,4 @@ from .designer import Designer from .category import Category from .contract_customer import ContractCustomer from .inquiry import Inquiry -from .product import Material +from .product import Material, Product diff --git a/api/models/inquiry.py b/api/models/inquiry.py index c64f469..344c54b 100644 --- a/api/models/inquiry.py +++ b/api/models/inquiry.py @@ -1,14 +1,57 @@ # coding=UTF-8 from api.extensions import db +from api.models import market as market_model +from api.lib.utils import round_half_up class Inquiry(db.Model): __tablename__ = 'inquiries' - inquiryid = db.Column(db.Integer, primary_key=True) + id = db.Column('inquiryid', db.Integer, primary_key=True) territory = db.Column(db.String(2)) product_group = db.Column(db.String(100)) + price_m2 = db.Column(db.Numeric) + price_effect = db.Column(db.Numeric) price_retouch = db.Column(db.Numeric) + width = db.Column(db.Integer) + height = db.Column(db.Integer) framed = db.Column(db.Integer, nullable=False, default=0) + dealer_store_id = db.Column('dealer_store', db.Integer, db.ForeignKey( + 'contract_customers.contractcustomerid')) + dealer_store = db.relationship('ContractCustomer', uselist=False) + framed = db.Column(db.Integer, default=0, nullable=False) + materialid = db.Column(db.Integer, db.ForeignKey( + 'product-materials.materialid'), default=1, nullable=False) + material = db.relationship('Material', uselist=False) pricepremium = db.Column(db.Numeric, nullable=False, default=0) + productId = db.Column(db.Integer, db.ForeignKey( + 'product-products.productid')) + product = db.relationship('Product', uselist=False) + automated = db.Column(db.Integer, nullable=False, default=0) + + @property + def market(self): + market = market_model.from_territory(self.territory) + if not market: + raise ValueError('Invalid territory: {}'.format(self.territory)) + return market + + def local_price_retouch(self, rounded=True, inc_vat=False): + """ returns the retouch price in the inquiry local currency """ + if not self.price_retouch: + return 0 + price = float(self.price_retouch) + price *= self.market.exchange_rate + price *= self.market.price_adjustments + if inc_vat: + price *= self.market.vat + if rounded: + price = round_half_up(price) + return price + + @property + def designer(self): + if self.product and self.product.designer: + return self.product.designer + return None diff --git a/api/models/product.py b/api/models/product.py index 69f0d76..7cbdb39 100644 --- a/api/models/product.py +++ b/api/models/product.py @@ -4,6 +4,15 @@ from api.extensions import db from flask_sqlalchemy import BaseQuery +class Product(db.Model): + + __tablename__ = 'product-products' + + id = db.Column('productid', db.Integer, primary_key=True) + designerid = db.Column(db.Integer, db.ForeignKey('designers.designerid')) + designer = db.relationship('Designer', backref='products') + + class Currencies(db.Model): __tablename__ = 'product-currencies' diff --git a/api/resources/prices.py b/api/resources/prices.py index e8d1caf..3407716 100644 --- a/api/resources/prices.py +++ b/api/resources/prices.py @@ -1,15 +1,14 @@ from flask import Blueprint, request, jsonify, abort from api.helpers import check_api_key from api.validators import validate_territory, validate_number +from api.models.inquiry import Inquiry from api.models.product import Material from api.models.contract_customer import ContractCustomer from api.models import market as market_model -from api.lib.prices import wallpaper_price, old_canvas_price, old_canvas_diy_frame_price, m2_price +from api.lib.prices import wallpaper_price, old_canvas_price, old_canvas_diy_frame_price, m2_price, inquiry_price from api.lib.limits import calculate_canvas_limits - mod = Blueprint('prices', __name__, url_prefix='/prices') - mod.before_request(check_api_key) @@ -90,3 +89,89 @@ def diy_frame(): }] return jsonify(data=data) + + +def get_inquiry_measurements(inquiry): + """ this function returns width and height to be used in the price calculations """ + + # first try to get it from the query string. If that fails try to get it + # from the inquiry row in the database + width = request.args.get('width', type=int, default=inquiry.width) + height = request.args.get('height', type=int, default=inquiry.height) + + # if we still are missing a width/height just set them to 1 to avoid + # errors in the price calculations + if not width: + width = 1 + + if not height: + height = 1 + + return width, height + + +def inquiry_wallpaper(inquiry): + width, height = get_inquiry_measurements(inquiry) + + data = { + 'id': inquiry.id, + 'group': 'wallpaper', + 'width': width, + 'height': height, + 'retouch_price': inquiry.local_price_retouch(rounded=False, inc_vat=True), + } + materials = Material.query.wallpapers().all() + prices = [] + for material in materials: + prices.append({ + 'material': material.name, + 'material_id': material.id, + 'm2_price': m2_price(material, inquiry.market, designer=inquiry.designer, reseller=inquiry.dealer_store, inquiry=inquiry, rounded=False, inc_vat=True), + 'inquiry_price_excl_retouch': inquiry_price(inquiry, material=material, width=width, height=height, rounded=False, inc_price_retouch=False, inc_vat=True), + 'inquiry_price': inquiry_price(inquiry, material=material, width=width, height=height, rounded=False, inc_vat=True), + 'inquiry_price_excl_vat': inquiry_price(inquiry, material=material, width=width, height=height, rounded=False, inc_vat=False) + }) + data['prices'] = prices + return data + + +def inquiry_canvas(inquiry): + in_width, in_height = get_inquiry_measurements(inquiry) + + data = { + 'id': inquiry.id, + 'group': 'canvas', + 'retouch_price': inquiry.local_price_retouch(rounded=False, inc_vat=True) + } + material = Material.query.canvas().first() + + prices = [] + + for framed in [True, False]: + width, height = calculate_canvas_limits(in_width, in_height, framed) + prices.append({ + 'framed': framed, + 'width': width, + 'height': height, + 'm2_price': m2_price(material, inquiry.market, designer=inquiry.designer, reseller=inquiry.dealer_store, inquiry=inquiry, rounded=False, inc_vat=True), + 'inquiry_price_excl_retouch': inquiry_price(inquiry, material=material, width=width, height=height, framed=framed, rounded=False, inc_price_retouch=False, inc_vat=True), + 'inquiry_price': inquiry_price(inquiry, material=material, width=width, height=height, framed=framed, rounded=False, inc_vat=True), + 'inquiry_price_excl_vat': inquiry_price(inquiry, material=material, width=width, height=height, framed=framed, rounded=False, inc_vat=False) + }) + data['prices'] = prices + + return data + + +@mod.route('/inquiry/', methods=["GET"]) +def inquiry(inquiry_id): + inquiry = Inquiry.query.get_or_404(inquiry_id) + + if inquiry.product_group == 'photo-wallpaper': + data = inquiry_wallpaper(inquiry) + elif inquiry.product_group == 'canvas': + data = inquiry_canvas(inquiry) + else: + raise ValueError('Invalid group {}'.format(inquiry.product_group)) + + return jsonify(data) diff --git a/docs/prices.md b/docs/prices.md index eb9c564..33b5f78 100644 --- a/docs/prices.md +++ b/docs/prices.md @@ -8,7 +8,7 @@ Calculate price inc VAT of a wallpaper product. The response will contain prices | Parameter | Description | | --------- | -----| -------- | - -| `width` | Wallpaper width in cm e.g `100' | +| `width` | Wallpaper width in cm e.g `100` | | `height` | Wallpaper height in cm e.g `50` | | `territory` | Two letter iso code e.g `SE`. Territory affect the price in different ways like VAT, currency exchanges etc. | | `dealerurl` | Optional parameter e.g `ackes`. Should always be used when calling the api from a dealerstore. If a dealerstore exists with this url additional fees will be applied to the price. | @@ -44,7 +44,7 @@ Calculate price inc VAT of a canvas product. The response will contain informati | Parameter | Description | | --------- | -----| -------- | - -| `width` | Canvas width in cm e.g `100' | +| `width` | Canvas width in cm e.g `100` | | `height` | Canvas height in cm e.g `50` | | `territory` | Two letter iso code e.g `SE`. Territory affect the price in different ways like VAT, currency exchanges etc. | | `dealerurl` | Optional parameter e.g `ackes`. Should always be used when calling the api from a dealerstore. If a dealerstore exists with this url additional fees will be applied to the price. | @@ -81,7 +81,7 @@ Calculate the price for the "do it yourself frame" (DIY). Note that width and he | Parameter | Description | | --------- | -----| -------- | - -| `width` | Canvas width in cm e.g `100'. | +| `width` | Canvas width in cm e.g `100` | | `height` | Canvas height in cm e.g `50` | | `territory` | Two letter iso code e.g `SE`. Territory affect the price in different ways like VAT, currency exchanges etc. | | `dealerurl` | Optional parameter e.g `ackes`. Should always be used when calling the api from a dealerstore. If a dealerstore exists with this url additional fees will be applied to the price. | @@ -102,4 +102,81 @@ Sample response } ] } +``` + +## Calculate inquiry price + +Calculates the price of an inquiry. For wallpapers inquiries the response will contain price information for all available materials and for canvas it will contain the price for both the framed and the not-framed canvas. The `group` attribute tells which kind of product you are dealing with. +The width and height stored on the inquiry will be used in the calculations by default. If you want to override these values you can send in your own width/height parameters in the request. + +| Parameter | Description | +| --------- | -----| -------- | - +| `width` | Width in cm e.g `100`. Defaults to the inquiry stored width | +| `height` | Height in cm e.g `50`. Defaults to the inquiry stored height | + + +Wallpaper inquiry - sample request + +`curl -X GET 'https://api2.photowall.com/prices/inquiry/41015` + +Sample response +``` +{ + "group": "wallpaper", + "height": 600, + "id": 41015, + "prices": [ + { + "inquiry_price": 3595.50219975, + "inquiry_price_excl_retouch": 3595.50219975, + "inquiry_price_excl_vat": 2876.4017598, + "material": "standard-wallpaper", + "material_id": 1, + "m2_price": 268.72213750000003 + }, + { + "inquiry_price": 3961.1464912499996, + "inquiry_price_excl_retouch": 3961.1464912499996, + "inquiry_price_excl_vat": 3168.9171929999998, + "material": "premium-wallpaper", + "material_id": 4, + "m2_price": 296.04981250000003 + } + ], + "retouch_price": 0, + "width": 223 +} +``` + +Canvas inquiry - sample request + +`curl -X GET 'https://api2.photowall.com/prices/inquiry/310999?width=500&height=150` + +Sample response +``` +{ + "group": "canvas", + "id": 310999, + "prices": [ + { + "framed": true, + "height": 150, + "inquiry_price": 356.25, + "inquiry_price_excl_retouch": 356.25, + "inquiry_price_excl_vat": 285, + "m2_price": 112.887, + "width": 150 + }, + { + "framed": false, + "height": 150, + "inquiry_price": 846.25, + "inquiry_price_excl_retouch": 846.25, + "inquiry_price_excl_vat": 677, + "m2_price": 112.887, + "width": 500 + } + ], + "retouch_price": 0 +} ``` \ No newline at end of file diff --git a/tests/config.py b/tests/config.py index c84d656..cf327a1 100644 --- a/tests/config.py +++ b/tests/config.py @@ -6,4 +6,4 @@ ESALES_URL = '' TESTING = True DEBUG = True SQLALCHEMY_ECHO = False -API_KEYS = None # api keys are disabled in test \ No newline at end of file +API_KEYS = None # api keys are disabled in test diff --git a/tests/lib/test_prices.py b/tests/lib/test_prices.py index 5654131..33cf563 100644 --- a/tests/lib/test_prices.py +++ b/tests/lib/test_prices.py @@ -4,6 +4,8 @@ import unittest2 from api.lib.prices import * from tests.factories import DesignerFactory, ContractCustomerFactory, InquiryFactory from api.models import market +from api.models import Inquiry +from api.models import Product from api.models.product import Material, MaterialPrice @@ -19,6 +21,10 @@ us = market.from_territory('US') us.price_adjustments = 1.05 us.exchange_rate = 0.119800 +nl = market.from_territory('NL') +nl.price_adjustments = 1.15 +nl.exchange_rate = 0.100000 + russia = market.from_territory('RU') russia.price_adjustments = 1 russia.exchange_rate = 0.100000 # russia uses EUR @@ -58,6 +64,9 @@ class TestPrice(unittest2.TestCase): material=standard_canvas, market=russia, designer=designer_moomin, rounded=False, inc_vat=False), places=4) self.assertAlmostEqual(110.88899937662403, m2_price(material=standard_canvas, market=russia, designer=designer_moomin, reseller=piterra, rounded=False, inc_vat=False), places=4) + inquiry = Inquiry(pricepremium=36.029411) + self.assertEqual(44, m2_price(material=standard_wallpaper, + market=russia, reseller=piterra, inquiry=inquiry, inc_vat=False)) def test_wallpaper_price(self): self.assertEqual(295, wallpaper_price( @@ -156,7 +165,79 @@ class TestPrice(unittest2.TestCase): width=50, height=50, material=standard_canvas, market=sweden, framed=True)) self.assertEqual(2972, old_canvas_price( width=150, height=140, material=standard_canvas, market=sweden, framed=True)) + + self.assertEqual(168, old_canvas_price(width=100, height=100, material=standard_canvas, market=russia, framed=True, + designer=designer_moomin, reseller=piterra)) + self.assertEqual(287, old_canvas_price( width=50, height=50, material=standard_canvas, market=sweden, framed=False, reseller=reseller)) self.assertEqual(111, old_canvas_price(width=100, height=100, material=standard_canvas, market=russia, framed=False, designer=designer_moomin, reseller=piterra)) + + def test_wallpaper_custom_inquiry_price(self): + inquiry = Inquiry(product_group="photo-wallpaper", territory="NL", pricepremium=0, + width=350, height=270, price_effect=0, price_retouch=160.00, material=standard_wallpaper) + + self.assertEqual(333, inquiry_price(inquiry)) + self.assertEqual(310, inquiry_price(inquiry, inc_price_retouch=False)) + self.assertEqual(160, inquiry.price_retouch) + self.assertEqual(18.4, inquiry.local_price_retouch(rounded=False)) + self.assertEqual(22, inquiry.local_price_retouch( + rounded=True, inc_vat=True)) + + # test with different material + self.assertEqual(342, inquiry_price( + inquiry, inc_price_retouch=False, material=premium_wallpaper)) + self.assertEqual(364, inquiry_price( + inquiry, material=premium_wallpaper)) + + def test_wallpaper_product_inquiry_price(self): + designer = DesignerFactory() + designer.pricepremium = 4 + product = Product(designer=designer) + inquiry = Inquiry(product_group="photo-wallpaper", territory="SE", pricepremium=0, + width=350, height=280, price_retouch=0, material=standard_wallpaper, product=product) + + self.assertEqual(3007, inquiry_price(inquiry)) + self.assertEqual(3007, inquiry_price(inquiry, inc_price_retouch=False)) + self.assertEqual(3312, inquiry_price( + inquiry, material=premium_wallpaper)) + + def test_wallpaper_product2_inquiry_price(self): + designer = DesignerFactory() + designer.pricepremium = 4 + product = Product(designer=designer) + inquiry = Inquiry(product_group="photo-wallpaper", territory="SE", pricepremium=25, + width=250, height=400, price_retouch=200, material=standard_wallpaper, product=product) + + self.assertEqual(3835, inquiry_price(inquiry, inc_price_retouch=False)) + self.assertEqual(200, inquiry.price_retouch) + self.assertEqual(250, inquiry.local_price_retouch( + rounded=True, inc_vat=True)) + + def test_wallpaper_piterra_inquiry_price(self): + product = Product(designer=None) + inquiry = Inquiry(product_group="photo-wallpaper", territory="RU", width=150, height=211, dealer_store=piterra, pricepremium=36.029411, + material=standard_wallpaper, product=product) + + self.assertEqual(138, inquiry_price(inquiry, inc_price_retouch=False)) + self.assertEqual(152, inquiry_price( + inquiry, inc_price_retouch=False, material=premium_wallpaper)) + + def test_canvas_custom_inquiry_price(self): + inquiry = Inquiry(product_group='canvas', territory='SE', width=100, height=100, + framed=True, price_retouch=200, pricepremium=0, material=standard_canvas) + + self.assertEqual(1520, inquiry_price(inquiry, inc_price_retouch=False)) + self.assertEqual(1770, inquiry_price(inquiry, inc_price_retouch=True)) + self.assertEqual(999, inquiry_price( + inquiry, inc_price_retouch=False, framed=False)) + + def test_canvas_piterra_inquiry_price(self): + inquiry = Inquiry(product_group='canvas', territory='RU', width=100, height=100, + framed=True, dealer_store=piterra, pricepremium=0, material=standard_canvas) + + # note: site currently calculates this as €150 which is wrong because + # it does not add dealer_store.pricepremium to the frame price + self.assertEqual(165, inquiry_price(inquiry)) + self.assertEqual(109, inquiry_price(inquiry, framed=False)) diff --git a/tests/resources/test_prices.py b/tests/resources/test_prices.py index 31621b6..3e455a9 100644 --- a/tests/resources/test_prices.py +++ b/tests/resources/test_prices.py @@ -2,6 +2,7 @@ import json import flask_testing from api import create_app, db from api.models.product import Material, MaterialPrice +from api.models.inquiry import Inquiry class TestEndpoints(flask_testing.TestCase): @@ -23,6 +24,13 @@ class TestEndpoints(flask_testing.TestCase): db.session.commit() return material + def _add_inquiry(self, territory, product_group, price_retouch=100): + inquiry = Inquiry( + territory=territory, product_group=product_group, price_retouch=price_retouch) + db.session.add(inquiry) + db.session.commit() + return inquiry + def test_wallpaper(self): self._add_material('standard-wallpaper', 1, 236) self._add_material('premium-wallpaper', 1, 260) @@ -33,7 +41,7 @@ class TestEndpoints(flask_testing.TestCase): self.assertEqual(2, len(products)) def test_canvas(self): - self._add_material('standard-canvas', 2, 799.9) + self._add_material('standard-canvas', 2, 799.2) response = self.client.get( '/prices/canvas?width=200&height=10&territory=SE') self.assert200(response) @@ -57,3 +65,18 @@ class TestEndpoints(flask_testing.TestCase): self.assertEqual(150, product['width'], "it adjusts measurements") self.assertEqual(50, product['height']) self.assertAlmostEqual(923.857, product['price'], places=2) + + def test_inquiry_wallpaper(self): + self._add_material('standard-wallpaper', 1, 236) + self._add_material('premium-wallpaper', 1, 260) + self._add_inquiry(territory='SE', product_group='photo-wallpaper') + response = self.client.get('/prices/inquiry/1?width=100&height=100') + self.assert200(response) + self.assertEqual('wallpaper', response.json['group']) + + def test_inquiry_canvas(self): + self._add_material('standard-canvas', 2, 799.2) + self._add_inquiry(territory='SE', product_group='canvas') + response = self.client.get('/prices/inquiry/1?width=100&height=100') + self.assert200(response) + self.assertEqual('canvas', response.json['group'])