diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bfd7ac5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,yml,yaml,rst,md}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..9429bc6 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,187 @@ +env: + browser: true + es6: true + +# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 +parserOptions: + ecmaVersion: 2019 + +overrides: + - files: + - "**/*.esm.js" + parserOptions: + sourceType: module + +# Globals available in Odoo that shouldn't produce errorings +globals: + _: readonly + $: readonly + fuzzy: readonly + jQuery: readonly + moment: readonly + odoo: readonly + openerp: readonly + owl: readonly + +# Styling is handled by Prettier, so we only need to enable AST rules; +# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890 +rules: + accessor-pairs: warn + array-callback-return: warn + callback-return: warn + capitalized-comments: + - warn + - always + - ignoreConsecutiveComments: true + ignoreInlineComments: true + complexity: + - warn + - 15 + constructor-super: warn + dot-notation: warn + eqeqeq: warn + global-require: warn + handle-callback-err: warn + id-blacklist: warn + id-match: warn + init-declarations: error + max-depth: warn + max-nested-callbacks: warn + max-statements-per-line: warn + no-alert: warn + no-array-constructor: warn + no-caller: warn + no-case-declarations: warn + no-class-assign: warn + no-cond-assign: error + no-const-assign: error + no-constant-condition: warn + no-control-regex: warn + no-debugger: error + no-delete-var: warn + no-div-regex: warn + no-dupe-args: error + no-dupe-class-members: error + no-dupe-keys: error + no-duplicate-case: error + no-duplicate-imports: error + no-else-return: warn + no-empty-character-class: warn + no-empty-function: error + no-empty-pattern: error + no-empty: warn + no-eq-null: error + no-eval: error + no-ex-assign: error + no-extend-native: warn + no-extra-bind: warn + no-extra-boolean-cast: warn + no-extra-label: warn + no-fallthrough: warn + no-func-assign: error + no-global-assign: error + no-implicit-coercion: + - warn + - allow: ["~"] + no-implicit-globals: warn + no-implied-eval: warn + no-inline-comments: warn + no-inner-declarations: warn + no-invalid-regexp: warn + no-irregular-whitespace: warn + no-iterator: warn + no-label-var: warn + no-labels: warn + no-lone-blocks: warn + no-lonely-if: error + no-mixed-requires: error + no-multi-str: warn + no-native-reassign: error + no-negated-condition: warn + no-negated-in-lhs: error + no-new-func: warn + no-new-object: warn + no-new-require: warn + no-new-symbol: warn + no-new-wrappers: warn + no-new: warn + no-obj-calls: warn + no-octal-escape: warn + no-octal: warn + no-param-reassign: warn + no-path-concat: warn + no-process-env: warn + no-process-exit: warn + no-proto: warn + no-prototype-builtins: warn + no-redeclare: warn + no-regex-spaces: warn + no-restricted-globals: warn + no-restricted-imports: warn + no-restricted-modules: warn + no-restricted-syntax: warn + no-return-assign: error + no-script-url: warn + no-self-assign: warn + no-self-compare: warn + no-sequences: warn + no-shadow-restricted-names: warn + no-shadow: warn + no-sparse-arrays: warn + no-sync: warn + no-this-before-super: warn + no-throw-literal: warn + no-undef-init: warn + no-undef: error + no-unmodified-loop-condition: warn + no-unneeded-ternary: error + no-unreachable: error + no-unsafe-finally: error + no-unused-expressions: error + no-unused-labels: error + no-unused-vars: error + no-use-before-define: error + no-useless-call: warn + no-useless-computed-key: warn + no-useless-concat: warn + no-useless-constructor: warn + no-useless-escape: warn + no-useless-rename: warn + no-void: warn + no-with: warn + operator-assignment: [error, always] + prefer-const: warn + radix: warn + require-yield: warn + sort-imports: warn + spaced-comment: [error, always] + strict: [error, function] + use-isnan: error + valid-jsdoc: + - warn + - prefer: + arg: param + argument: param + augments: extends + constructor: class + exception: throws + func: function + method: function + prop: property + return: returns + virtual: abstract + yield: yields + preferType: + array: Array + bool: Boolean + boolean: Boolean + number: Number + object: Object + str: String + string: String + requireParamDescription: false + requireReturn: false + requireReturnDescription: false + requireReturnType: false + valid-typeof: warn + yoda: warn diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index 86ed03f..0000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Build VSF - -on: - push: - branches: [ 16.0 ] - -jobs: - deployment: - runs-on: self-hosted - steps: - - run: | - echo "-------- Deploying https://vsfdemo16.labs.odoogap.com/ " - /home/egap/.scripts/update diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..057ad98 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,15 @@ +--- +name: pre-commit + +on: + push: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4.4.0 + with: + python-version: "3.10" + - uses: pre-commit/action@v3.0.0 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..0ec187e --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,13 @@ +[settings] +; see https://github.com/psf/black +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +combine_as_imports=True +use_parentheses=True +line_length=88 +known_odoo=odoo +known_odoo_addons=odoo.addons +sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER +default_section=THIRDPARTY +ensure_newline_before_comments = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2ff26fd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,150 @@ +--- +exclude: | + (?x) + # Files and folders generated by bots, to avoid loops + ^setup/|/static/description/index\.html$| + # We don't want to mess with tool-generated files + .svg$| + # Ignore readmes + ^README\.md$| + ^README\.rst$| + # Library files can have extraneous formatting (even minimized) + /static/(src/)?lib/| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*)| + # Do not check symlinked submodule directories. + ^graphql_base/ +default_language_version: + python: python3 + node: "14.18.0" +repos: + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.4.1 + hooks: + - id: prettier + name: prettier (with plugin-xml) + additional_dependencies: + - "prettier@2.4.1" + - "@prettier/plugin-xml@1.1.0" + args: + - --plugin=@prettier/plugin-xml + - --xml-self-closing-space=false + files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: ["flake8-bugbear==22.9.11"] + - repo: https://github.com/myint/autoflake + rev: v1.6.0 + hooks: + - id: autoflake + args: + - --expand-star-imports + - --ignore-init-module-imports + - --in-place + - --remove-all-unused-imports + - --remove-duplicate-keys + - --remove-unused-variables + - repo: https://github.com/asottile/pyupgrade + rev: v2.38.0 + hooks: + - id: pyupgrade + args: + - --keep-percent-format + - --py310-plus + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort except __init__.py + args: + - --settings=. + exclude: /__init__\.py$ + - repo: https://github.com/PyCQA/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + args: + - --config=setup.cfg + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-no-eval + - id: python-no-log-warn + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.23.1 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + entry: bandit + language: python + language_version: python3 + types: [python] + args: + - --ini=setup.cfg + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets + - repo: https://github.com/adrienverge/yamllint + rev: v1.28.0 + hooks: + - id: yamllint + entry: yamllint + language: python + types: [file, yaml] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: check-ast + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + exclude: \.sh\.jinja2$|\.sh\.j2$ + - id: check-json + - id: check-xml + - id: check-yaml + args: + # won't load yaml, only validate it (workaround for ansible !vault) + - --unsafe + - id: check-case-conflict + - id: check-merge-conflict + # exclude files where underlines are not distinguishable from merge + # conflicts + exclude: /README\.rst$|^docs/.*\.rst$ + - id: check-symlinks + - id: debug-statements + # Fixers + - id: end-of-file-fixer + - id: fix-byte-order-marker + # To remove utf-8 encoding (and the like) in python files. + - id: fix-encoding-pragma + args: ["--remove"] + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/OCA/pylint-odoo + rev: v8.0.17 + hooks: + - id: pylint_odoo + args: + - --rcfile=.pylintrc-mandatory + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + args: + - --config=.python-black diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..177e347 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,9 @@ +# Defaults for all prettier-supported languages. +# Prettier will complete this with settings from .editorconfig file. +--- +bracketSpacing: false +printWidth: 88 +proseWrap: always +semi: true +trailingComma: "es5" +xmlWhitespaceSensitivity: "strict" diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory new file mode 100644 index 0000000..14e6114 --- /dev/null +++ b/.pylintrc-mandatory @@ -0,0 +1,96 @@ + +[MASTER] +load-plugins=pylint_odoo +score=n + +[ODOOLINT] +readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst" +manifest_required_authors=Odoo Community Association (OCA) +manifest_required_keys=license +manifest_deprecated_keys=description,active +license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3 +valid_odoo_versions=16.0 + +[MESSAGES CONTROL] +disable=all + +enable=anomalous-backslash-in-string, + api-one-deprecated, + api-one-multi-together, + assignment-from-none, + attribute-deprecated, + class-camelcase, + dangerous-default-value, + dangerous-view-replace-wo-priority, + development-status-allowed, + duplicate-id-csv, + duplicate-key, + duplicate-xml-fields, + duplicate-xml-record-id, + eval-referenced, + eval-used, + incoherent-interpreter-exec-perm, + license-allowed, + manifest-author-string, + manifest-deprecated-key, + manifest-required-key, + manifest-version-format, + method-compute, + method-inverse, + method-required-super, + method-search, + openerp-exception-warning, + pointless-statement, + pointless-string-statement, + print-used, + redundant-keyword-arg, + redundant-modulename-xml, + reimported, + relative-import, + return-in-init, + rst-syntax-error, + sql-injection, + too-few-format-args, + translation-field, + translation-required, + unreachable, + use-vim-comment, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + attribute-string-redundant, + character-not-valid-in-resource-link, + consider-merging-classes-inherited, + context-overridden, + create-user-wo-reset-password, + dangerous-filter-wo-user, + dangerous-qweb-replace-wo-priority, + deprecated-data-xml-node, + deprecated-openerp-xml-node, + duplicate-po-message-definition, + except-pass, + file-not-used, + invalid-commit, + manifest-maintainers-list, + missing-newline-extrafiles, + missing-readme, + odoo-addons-relative-import, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + renamed-field-parameter, + resource-not-exist, + str-format-used, + test-folder-imported, + translation-contains-variable, + translation-positional-used, + unnecessary-utf8-coding-comment, + website-manifest-key-not-valid-uri, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + external-request-timeout + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/.python-black b/.python-black new file mode 100644 index 0000000..28f4e25 --- /dev/null +++ b/.python-black @@ -0,0 +1 @@ +[tool.black] diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1 @@ +{} diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..56684ed --- /dev/null +++ b/.yamllint @@ -0,0 +1,3 @@ +rules: + comments: + min-spaces-from-content: 1 diff --git a/graphql_base b/graphql_base deleted file mode 120000 index 664abdd..0000000 --- a/graphql_base +++ /dev/null @@ -1 +0,0 @@ -.external/rest-framework/graphql_base/ \ No newline at end of file diff --git a/graphql_vuestorefront/README.rst b/graphql_vuestorefront/README.rst deleted file mode 100644 index 103ac5f..0000000 --- a/graphql_vuestorefront/README.rst +++ /dev/null @@ -1,116 +0,0 @@ -====================== -Graphql Vue Storefront -====================== - -Login -===== - -To authenticate, use the default /web/session/authenticate endpoint. -Example using axios: - -axios.post('/web/session/authenticate', { - "jsonrpc": "2.0", - "method": "call", - "params": { - "db": , - "login": , - "password": -}}, { - "withCredentials": true -}) - -Logout -====== - -axios.post('/web/session/destroy', { - "jsonrpc": "2.0", - "method": "call" -}, { - "withCredentials": true -}) - -Add to Cart -=========== - -axios.post('/shop/cart/update_json', { - "jsonrpc": "2.0", - "method": "call", - "params": { - "product_id": , - "add_qty": -}}, { - "withCredentials": true -}) - -Add to wishlist -=============== - -axios.post('/shop/wishlist/add', { - "jsonrpc": "2.0", - "method": "call", - "params": { - "product_id": , -}}, { - "withCredentials": true -}) - -Remove from wishlist -==================== - -axios.post('/shop/wishlist/remove/', { - "jsonrpc": "2.0", - "method": "call" -}, { - "withCredentials": true -}) - -Get the rate for a shipping method -================================== - -axios.post('/shop/carrier_rate_shipment', { - "jsonrpc": "2.0", - "method": "call" - "params": { - "carrier_id": , -}}, { - "withCredentials": true -}) - -Get all product template attributes for product template page -============================================================= - -axios.post('/shop/get_combinations/', { - "jsonrpc": "2.0", - "method": "call" -}, { - "withCredentials": true -}) - -Get product id and price after selecting the combination on the product template page -===================================================================================== - -axios.post('/shop/get_combination_info/', { - "jsonrpc": "2.0", - "method": "call" - "params": { - "combination_ids": [1, 2], - add_qty=1 -}}, { - "withCredentials": true -}) - -Get products for shop with search, category, sort, count, pagination and attributes filtering -============================================================================================= - -axios.post('/shop/products', { - "jsonrpc": "2.0", - "method": "call" - "params": { - "search": "", - "category_id": 1, - "offset": 0, - "ppg": 20, - "attrib_list": [] -}}, { - "withCredentials": true -}) diff --git a/graphql_vuestorefront/__manifest__.py b/graphql_vuestorefront/__manifest__.py deleted file mode 100644 index cd2b361..0000000 --- a/graphql_vuestorefront/__manifest__.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -{ - 'name': 'Vue Storefront Api', - 'version': '16.0.1.0.0', - 'summary': 'Vue Storefront API', - 'description': """Vue Storefront API Integration""", - 'category': 'Website', - 'license': 'LGPL-3', - 'author': 'OdooGap', - 'website': 'https://www.odoogap.com/', - 'depends': [ - 'graphql_base', - 'website_sale_wishlist', - 'website_sale_delivery', - 'website_mass_mailing', - 'website_sale_loyalty', - 'auth_signup', - 'contacts', - 'crm', - 'theme_default', - 'payment_adyen_vsf', - ], - 'data': [ - 'security/ir.model.access.csv', - 'data/website_data.xml', - 'data/mail_template.xml', - 'data/ir_config_parameter_data.xml', - 'data/ir_cron_data.xml', - 'views/product_views.xml', - 'views/website_views.xml', - 'views/res_config_settings_views.xml', - ], - 'demo': [ - 'data/demo_product_attribute.xml', - 'data/demo_product_public_category.xml', - 'data/demo_products_women_clothing.xml', - 'data/demo_products_women_shoes.xml', - 'data/demo_products_women_bags.xml', - 'data/demo_products_men_clothing_1.xml', - 'data/demo_products_men_clothing_2.xml', - 'data/demo_products_men_clothing_3.xml', - 'data/demo_products_men_clothing_4.xml', - 'data/demo_products_men_shoes.xml', - ], - 'installable': True, - 'auto_install': False, - 'pre_init_hook': 'pre_init_hook_login_check', - 'post_init_hook': 'post_init_hook_login_convert', -} diff --git a/graphql_vuestorefront/controllers/main.py b/graphql_vuestorefront/controllers/main.py deleted file mode 100644 index 51e350a..0000000 --- a/graphql_vuestorefront/controllers/main.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import os -import json -import logging -import pprint - -from odoo import http -from odoo.addons.web.controllers.binary import Binary -from odoo.addons.graphql_base import GraphQLControllerMixin -from odoo.http import request, Response -from odoo.tools.safe_eval import safe_eval -from urllib.parse import urlparse - -from ..schema import schema - -_logger = logging.getLogger(__name__) - - -class VSFBinary(Binary): - @http.route(['/web/image', - '/web/image/', - '/web/image//', - '/web/image//x', - '/web/image//x/', - '/web/image///', - '/web/image////', - '/web/image////x', - '/web/image////x/', - '/web/image/', - '/web/image//', - '/web/image//x', - '/web/image//x/', - '/web/image/-', - '/web/image/-/', - '/web/image/-/x', - '/web/image/-/x/'], type='http', - auth="public") - def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas', - filename_field='name', unique=None, filename=None, mimetype=None, - download=None, width=0, height=0, crop=False, access_token=None, - **kwargs): - """ Validate width and height """ - try: - ICP = request.env['ir.config_parameter'].sudo() - vsf_image_resize_limit = int(ICP.get_param('vsf_image_resize_limit', 1920)) - - if width > vsf_image_resize_limit or height > vsf_image_resize_limit: - return request.not_found() - except Exception: - return request.not_found() - - return super(VSFBinary, self).content_image( - xmlid=xmlid, model=model, id=id, field=field, filename_field=filename_field, unique=unique, - filename=filename, mimetype=mimetype, download=download, width=width, height=height, crop=crop, - access_token=access_token, **kwargs) - - -class GraphQLController(http.Controller, GraphQLControllerMixin): - - def _process_request(self, schema, data): - # Set the vsf_debug_mode value that exist in the settings - ICP = http.request.env['ir.config_parameter'].sudo() - vsf_debug_mode = ICP.get_param('vsf_debug_mode', False) - if vsf_debug_mode: - try: - request = http.request.httprequest - _logger.info('# ------------------------------- GRAPHQL: DEBUG MODE -------------------------------- #') - _logger.info('') - _logger.info('# ------------------------------------------------------- #') - _logger.info('# HEADERS #') - _logger.info('# ------------------------------------------------------- #') - _logger.info('\n%s', pprint.pformat(request.headers.environ)) - _logger.info('') - _logger.info('# ------------------------------------------------------- #') - _logger.info('# QUERY / MUTATION #') - _logger.info('# ------------------------------------------------------- #') - _logger.info('\n%s', data.get('query', None)) - _logger.info('') - _logger.info('# ------------------------------------------------------- #') - _logger.info('# ARGUMENTS #') - _logger.info('# ------------------------------------------------------- #') - _logger.info('\n%s', request.args.get('variables', None)) - _logger.info('') - _logger.info('# ------------------------------------------------------------------------------------ #') - except: - pass - return super(GraphQLController, self)._process_request(schema, data) - - def _set_website_context(self): - """Set website context based on http_request_host header.""" - try: - request_host = request.httprequest.headers.environ['HTTP_RESQUEST_HOST'] - website = request.env['website'].search([('domain', 'ilike', request_host)], limit=1) - if website: - context = dict(request.context) - context.update({ - 'website_id': website.id, - 'lang': website.default_lang_id.code, - }) - request.context = context - - request_uid = http.request.env.uid - website_uid = website.sudo().user_id.id - - if request_uid != website_uid \ - and request.env['res.users'].sudo().browse(request_uid).has_group('base.group_public'): - request.uid = website_uid - except: - pass - - # The GraphiQL route, providing an IDE for developers - @http.route("/graphiql/vsf", auth="user") - def graphiql(self, **kwargs): - self._set_website_context() - return self._handle_graphiql_request(schema.graphql_schema) - - # The graphql route, for applications. - # Note csrf=False: you may want to apply extra security - # (such as origin restrictions) to this route. - @http.route("/graphql/vsf", auth="public", csrf=False) - def graphql(self, **kwargs): - self._set_website_context() - return self._handle_graphql_request(schema.graphql_schema) - - @http.route('/vsf/categories', type='http', auth='public', csrf=False) - def vsf_categories(self): - self._set_website_context() - website = request.env['website'].get_current_website() - - categories = [] - - if website.default_lang_id: - lang_code = website.default_lang_id.code - domain = [('website_slug', '!=', False)] - - for category in request.env['product.public.category'].sudo().search(domain): - category = category.with_context(lang=lang_code) - categories.append(category.website_slug) - - return Response( - json.dumps(categories), - headers={'Content-Type': 'application/json'}, - ) - - @http.route('/vsf/products', type='http', auth='public', csrf=False) - def vsf_products(self): - self._set_website_context() - website = request.env['website'].get_current_website() - - products = [] - - if website.default_lang_id: - lang_code = website.default_lang_id.code - domain = [('website_published', '=', True), ('website_slug', '!=', False)] - - for product in request.env['product.template'].sudo().search(domain): - product = product.with_context(lang=lang_code) - - url_parsed = urlparse(product.website_slug) - name = os.path.basename(url_parsed.path) - path = product.website_slug.replace(name, '') - - products.append({ - 'name': name, - 'path': '{}:slug'.format(path), - }) - - return Response( - json.dumps(products), - headers={'Content-Type': 'application/json'}, - ) - - @http.route('/vsf/redirects', type='http', auth='public', csrf=False) - def vsf_redirects(self): - redirects = [] - - for redirect in request.env['website.rewrite'].sudo().search([]): - redirects.append({ - 'from': redirect.url_from, - 'to': redirect.url_to, - }) - - return Response( - json.dumps(redirects), - headers={'Content-Type': 'application/json'}, - ) diff --git a/graphql_vuestorefront/data/demo_products_men_clothing_1.xml b/graphql_vuestorefront/data/demo_products_men_clothing_1.xml deleted file mode 100644 index ceba7f5..0000000 --- a/graphql_vuestorefront/data/demo_products_men_clothing_1.xml +++ /dev/null @@ -1,929 +0,0 @@ - - - - - - - Daniele Alessandrini – Vest - - 165.00 - - 165.00 - product - - - - This is the product: Daniele Alessandrini - Vest - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DA01-01 - 165.00 - - - - DA01-02 - 165.00 - - - - DA01-03 - 165.00 - - - - DA01-04 - 165.00 - - - - DA01-05 - 165.00 - 170.00 - - - - DA01-06 - 165.00 - 170.00 - - - - - - - Leather jacket Bully dark blue - - 523.75 - - 523.75 - product - - - - This is the product: Leather jacket Bully dark blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LJBD01-01 - 523.75 - - - - LJBD01-02 - 523.75 - - - - - - - Bomber Daniele Alessandrini blue - - 356.25 - - 356.25 - product - - - - This is the product: Bomber Daniele Alessandrini blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BDA01-01 - 523.75 - - - - BDA01-02 - 523.75 - - - - - - - Save the Duck – Casual Jacket - - 161.25 - - 161.25 - product - - - - This is the product: Save the Duck – Casual Jacket - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CJ01-01 - 161.25 - - - - CJ01-02 - 161.25 - - - - CJ01-03 - 161.25 - - - - CJ01-04 - 161.25 - - - - CJ01-05 - 161.25 - - - - CJ01-06 - 161.25 - - - - - - - Vest ”Naples” Moncler red - - 662.50 - - 662.50 - product - - - - This is the product: Vest ”Naples” Moncler red - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VNMR01-01 - 662.50 - - - - VNMR01-02 - 662.50 - - - - VNMR01-03 - 662.50 - - - - VNMR01-04 - 662.50 - - - - - - - Moncler – Down Jacket “Jacob” - - 1218.75 - - 1218.75 - product - - - - This is the product: Moncler – Down Jacket “Jacob” - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MDJ01-01 - 1218.75 - - - - MDJ01-02 - 1218.75 - - - - MDJ01-03 - 1218.75 - - - - - - - - Casual jacket Aspesi blue - - 430.00 - - 430.00 - product - - - - This is the product: Casual jacket Aspesi blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CJB01-01 - 430.00 - - - - CJB01-02 - 430.00 - - - - - - - Casual jacket “Mahakali“ Peuterey blue - - 423.75 - - 423.75 - product - - - - This is the product: Casual jacket “Mahakali“ Peuterey blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Mahakali-01 - 423.75 - - - - Mahakali-02 - 423.75 - - - - - - - Casual jacket Invicta blue - - 173.75 - - 173.75 - product - - - - This is the product: Casual jacket Invicta blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Invicta-01 - 423.75 - - - - Invicta-02 - 423.75 - - - - - - - - Casual jacket ”Lyon” Moncler black - - 656.25 - - 656.25 - product - - - - This is the product: Casual jacket ”Lyon” Moncler black - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LYON-01 - 656.25 - - - - LYON-02 - 656.25 - - - - - diff --git a/graphql_vuestorefront/data/demo_products_men_clothing_2.xml b/graphql_vuestorefront/data/demo_products_men_clothing_2.xml deleted file mode 100644 index e06f83e..0000000 --- a/graphql_vuestorefront/data/demo_products_men_clothing_2.xml +++ /dev/null @@ -1,817 +0,0 @@ - - - - - - - Casual Jacket Save the Duck dark blue - - 198.75 - - 198.75 - product - - - - This is the product: Casual Jacket Save the Duck dark blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CJSDDB-01 - 198.75 - - - - - CJSDDB-02 - 198.75 - - - - - - - Leather jacket D.r.o.w.s black - - 872.50 - - 872.50 - product - - - - This is the product: Leather jacket D.r.o.w.s black - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LJB3-01 - 872.50 - - - - LJB3-02 - 872.50 - - - - - - - - Moncler – Down Jacket “Ryan” - - 1162.50 - - 1162.50 - product - - - - This is the product: Moncler – Down Jacket “Ryan” - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RYAN-01 - 1162.50 - - - - RYAN-02 - 1162.50 - - - - - - - Harris Wharf – Coat - - 598.75 - - 598.75 - product - - - - This is the product: Harris Wharf – Coat - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - HARRISWARF-01 - 598.75 - - - - HARRISWARF-02 - 598.75 - - - - - - - Casual jacket Michael Kors beige - - 373.75 - - 373.75 - product - - - - This is the product: Casual jacket Michael Kors beige - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MKB02-01 - 598.75 - - - - - MKB02-02 - 598.75 - - - - - - - - Down jacket “Kathmandu“ Peuterey grey - - 411.25 - - 411.25 - product - - - - This is the product: Down jacket “Kathmandu“ Peuterey grey - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - KATHMANDU-01 - 411.25 - - - - KATHMANDU-02 - 411.25 - - - - KATHMANDU-03 - 411.25 - - - - - - - - Coat Aspesi beige - - 536.25 - - 536.25 - product - - - - This is the product: Coat Aspesi beige - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CAD02-01 - 536.25 - - - - CAD02-02 - 536.25 - - - - - - - - Casual jacket Stone Island grey - - 837.50 - - 837.50 - product - - - - This is the product: Casual jacket Stone Island grey - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CJSIG-01 - 837.50 - - - - CJSIG-02 - 837.50 - - - - - - - Jacket Doubleface “Sol Walk“ Luis Trenker blue - - 498.75 - - 498.75 - product - - - - This is the product: Jacket Doubleface “Sol Walk“ Luis Trenker blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SOLWALK-01 - 498.75 - - - - SOLWALK-02 - 498.75 - - - - - - - Bully – Leather Jacket - - 497.50 - - 497.50 - product - - - - This is the product: Bully – Leather Jacket - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BULLY02-01 - 497.50 - - - - - BULLY02-02 - 497.50 - - - - diff --git a/graphql_vuestorefront/data/demo_products_men_clothing_3.xml b/graphql_vuestorefront/data/demo_products_men_clothing_3.xml deleted file mode 100644 index f47ee86..0000000 --- a/graphql_vuestorefront/data/demo_products_men_clothing_3.xml +++ /dev/null @@ -1,405 +0,0 @@ - - - - - - - Coat Aspesi blue - - 536.25 - - 536.25 - product - - - - This is the product: Coat Aspesi blue - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CAB05-01 - 536.25 - - - - CAB05-02 - 536.25 - - - - - - - - Casual jacket Stone Island black - - 498.75 - - 498.75 - product - - - - This is the product: Casual jacket Stone Island black - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CJSIB06-01 - 498.75 - - - - - CJSIB06-02 - 498.75 - - - - - - - Vest Bully black - - 486.25 - - 486.25 - product - - - - This is the product: Vest Bully black - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VBB03-01 - 486.25 - - - - VBB03-02 - 486.25 - - - - - - - Leather jacket Daniele Alessandrini beige - - 736.25 - - 736.25 - product - - - - This is the product: Leather jacket Daniele Alessandrini beige - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LJDAB02-01 - 736.25 - - - - LJDAB02-02 - 736.25 - - - - - - - Jacket Doubleface “Sol Walk“ Luis Trenker red - - 498.75 - - 498.75 - product - - - - This is the product: Jacket Doubleface “Sol Walk“ Luis Trenker red - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - JDSWLTR-01 - 498.75 - - - - JDSWLTR-02 - 498.75 - - - - - diff --git a/graphql_vuestorefront/data/demo_products_men_clothing_4.xml b/graphql_vuestorefront/data/demo_products_men_clothing_4.xml deleted file mode 100644 index 10c988f..0000000 --- a/graphql_vuestorefront/data/demo_products_men_clothing_4.xml +++ /dev/null @@ -1,966 +0,0 @@ - - - - - - - Cardigan Kangra green - - 200.00 - - 200.00 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 200.00 - - - 578902-00 - 200.00 - - - - - Vest Tagliatore blue - - 206.25 - - 206.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 206.25 - - - 578902-00 - 206.25 - - - - - - Shirt Barba blue - - 248.75 - - 248.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 248.75 - - - 578902-00 - 248.75 - - - - - Shirt Himons multi - - 148.75 - - 148.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 148.75 - - - 578902-00 - 148.75 - - - - - - Chino Paolo Pecora grey - - 281.25 - - 281.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 281.25 - - - 578902-00 - 281.25 - - - - - Daniele Alessandrini – Casual hosen - - 218.75 - - 218.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 218.75 - - - 578902-00 - 218.75 - - - - - - jeans Siviglia dark blue - - 243.75 - - 243.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 243.75 - - - 578902-00 - 243.75 - - - - - Jeans Closed grey - - 211.25 - - 211.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 211.25 - - - 578902-00 - 211.25 - - - - - - Blazer Tagliatore brown - - 573.75 - - 573.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 573.75 - - - 578902-00 - 573.75 - - - - - Blazer Circolo 1901 blue - - 411.25 - - 411.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 411.25 - - - 578902-00 - 411.25 - - - - - - Suit Mauro Grifoni grey - - 668.75 - - 668.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 668.75 - - - 578902-00 - 668.75 - - - - - Suit Tagliatore dark blue - - 862.50 - - 862.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 862.50 - - - 578902-00 - 862.50 - - - - - - Shirt The Sartorialist light blue - - 186.25 - - 186.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 186.25 - - - 578902-00 - 186.25 - - - - - Shirt Daniele Alessandrini grey - - 198.75 - - 198.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 198.75 - - - 578902-00 - 198.75 - - - diff --git a/graphql_vuestorefront/data/demo_products_men_shoes.xml b/graphql_vuestorefront/data/demo_products_men_shoes.xml deleted file mode 100644 index 46ca507..0000000 --- a/graphql_vuestorefront/data/demo_products_men_shoes.xml +++ /dev/null @@ -1,418 +0,0 @@ - - - - - - - Sneaker – Lotto “Tokyo“ - - 137.50 - - 137.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 137.50 - - - 578902-00 - 137.50 - - - - - Sneakers “Spot“ Springa multi - - 186.25 - - 186.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 186.25 - - - 578902-00 - 186.25 - - - - - - Lace up shoes Tods dark blue - - 462.50 - - 462.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 462.50 - - - 578902-00 - 462.50 - - - - - Lace up shoes Tods blue - - 362.50 - - 362.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 362.50 - - - 578902-00 - 362.50 - - - - - - Mokassins “Daime“ Doucals brown - - 343.75 - - 343.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 343.75 - - - 578902-00 - 343.75 - - - - - Flip Flops “Top Mix“ Havaianas dark blue - - 27.50 - - 27.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 27.50 - - - 578902-00 - 27.50 - - - diff --git a/graphql_vuestorefront/data/demo_products_women_bags.xml b/graphql_vuestorefront/data/demo_products_women_bags.xml deleted file mode 100644 index 112726e..0000000 --- a/graphql_vuestorefront/data/demo_products_women_bags.xml +++ /dev/null @@ -1,811 +0,0 @@ - - - - - - - Michael Kors – Clutch “Daria” - - 281.25 - - 281.25 - product - - - - This is the product: Michael Kors – Clutch “Daria” - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 281.25 - - - - - Clutch ”Carol” Liebeskind black - - 198.75 - - 198.75 - product - - - - This is the product: Clutch ”Carol” Liebeskind black - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 198.75 - - - - - - Clutch “Jet Set Travel” small Michael Kors - - 106.25 - - 106.25 - product - - - - This is the product: Clutch “Jet Set Travel” small Michael Kors - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CJSTMK-01 - 106.25 - - - - - CJSTMK-02 - 106.25 - - - - CJSTMK-03 - 106.25 - - - - CJSTMK-04 - 106.25 - - - - CJSTMK-05 - 106.25 - - - - CJSTMK-06 - 106.25 - - - - - CJSTMK-07 - 106.25 - - - - - CJSTMK-08 - 106.25 - - - - - - 31.25 - - - - - - Bag Moschino Love black-white - - 190.00 - - 190.00 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 190.00 - - - - - DKNY – Bag - - 372.50 - - 372.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 372.50 - - - - - - Moschino Love – Shopper - - 256.25 - - 256.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 256.25 - - - - - - Michael Kors – Shopper “Jet Set Travel” - - 343.75 - - 343.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 343.75 - - - - - - Bag “Jet Set Travel” Michael Kors - - 368.75 - - 368.75 - product - - - - This is the product: Bag “Jet Set Travel” Michael Kors - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BJSTMK-01 - 368.75 - - - - BJSTMK-01 - 368.75 - - - - - 26.00 - - - - - - Guess – Hand bag “Nikki“ - - 173.75 - - 173.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 173.75 - - - - - Guess – handtaschen “Carnivale“ - - 181.25 - - 181.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 181.25 - - - - - - Wallet “Pervinca“ Gabs white - - 102.50 - - 102.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 102.50 - - - - - Gabs – Wallet “Gmoney” - - 106.25 - - 106.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 106.25 - - - diff --git a/graphql_vuestorefront/data/demo_products_women_clothing.xml b/graphql_vuestorefront/data/demo_products_women_clothing.xml deleted file mode 100644 index ae2b9db..0000000 --- a/graphql_vuestorefront/data/demo_products_women_clothing.xml +++ /dev/null @@ -1,1240 +0,0 @@ - - - - - - - Leather jacket Bully grey - - 372.50 - - 372.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 372.50 - - - 578902-00 - 372.50 - - - - - Leather jacket Bully brown - - 372.50 - - 372.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 372.50 - - - 578902-00 - 372.50 - - - - - - Blazer Michael Kors brown - - 281.25 - - 281.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 281.25 - - - 578902-00 - 281.25 - - - - - Blazer Pinko yellow - - 337.50 - - 337.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 337.50 - - - 578902-00 - 337.50 - - - - - - Pullover Moschino Cheap And Chic black - - 247.50 - - 247.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 247.50 - - - 578902-00 - 247.50 - - - - - Pullover Moschino Cheap And Chic black - - 247.50 - - 247.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 247.50 - - - 578902-00 - 247.50 - - - - - - Shirt Aspesi white test - - 200.00 - - 200.00 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 200.00 - - - 578902-00 - 200.00 - - - - - Shirt Himons multi - - 148.75 - - 148.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 148.75 - - - 578902-00 - 148.75 - - - - - - Shirt ”Paola” MU blue - - 231.25 - - 231.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 231.25 - - - 578902-00 - 231.25 - - - - - Shirt Himons light blue - - 123.75 - - 123.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 123.75 - - - 578902-00 - 123.75 - - - - - - Biker jeans Pinko white - - 247.50 - - 247.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 247.50 - - - 578902-00 - 247.50 - - - - - jeans Michael Kors black - - 193.75 - - 193.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 193.75 - - - 578902-00 - 193.75 - - - - - - Jogging Pants Moschino Cheap And Chic black - - 286.25 - - 286.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 286.25 - - - 578902-00 - 286.25 - - - - - Chino Pinko multi - - 287.50 - - 287.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 287.50 - - - 578902-00 - 287.50 - - - - - - Skirt Ki 6? Who are you? blue - - 185.00 - - 185.00 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 185.00 - - - 578902-00 - 185.00 - - - - - Skirt Pinko beige - - 185.00 - - 185.00 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 185.00 - - - 578902-00 - 185.00 - - - - - - Dress Ki 6? Who are you? black - - 206.25 - - 206.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 206.25 - - - 578902-00 - 206.25 - - - - - Dress Moschino Cheap And Chic multi - - 320.00 - - 320.00 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 320.00 - - - 578902-00 - 320.00 - - - diff --git a/graphql_vuestorefront/data/demo_products_women_shoes.xml b/graphql_vuestorefront/data/demo_products_women_shoes.xml deleted file mode 100644 index c718e12..0000000 --- a/graphql_vuestorefront/data/demo_products_women_shoes.xml +++ /dev/null @@ -1,966 +0,0 @@ - - - - - - - Sneakers “Jazz“ Saucony grey-green - - 118.75 - - 118.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 118.75 - - - 578902-00 - 118.75 - - - - - Sneakers Philippe Model grey - - 327.50 - - 327.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 327.50 - - - 578902-00 - 327.50 - - - - - - Booclothing Lerews beige - - 186.25 - - 186.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 186.25 - - - 578902-00 - 186.25 - - - - - Booclothing Lerews black - - 186.25 - - 186.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 186.25 - - - 578902-00 - 186.25 - - - - - - Booties Lemare black - - 231.25 - - 231.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 231.25 - - - 578902-00 - 231.25 - - - - - Booties Lemare brown - - 248.75 - - 248.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 248.75 - - - 578902-00 - 248.75 - - - - - - Pumps “Okala“ Sam Edelman grey - - 186.25 - - 186.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 186.25 - - - 578902-00 - 186.25 - - - - - Pumps ”H228” Hogan blue - - 362.50 - - 362.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 362.50 - - - 578902-00 - 362.50 - - - - - - Ballerina Liebeskind black-beige - - 123.75 - - 123.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 123.75 - - - 578902-00 - 123.75 - - - - - Ballerina Liebeskind multi - - 98.75 - - 98.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 98.75 - - - 578902-00 - 98.75 - - - - - - Loafers Alberto Guardiani gold - - 343.75 - - 343.75 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 343.75 - - - 578902-00 - 343.75 - - - - - Slip-On Shoes Crime silver - - 161.25 - - 161.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 161.25 - - - 578902-00 - 161.25 - - - - - - Sandals ”H257” Hogan beige - - 337.50 - - 337.50 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 337.50 - - - 578902-00 - 337.50 - - - - - sandalen “Georgie“ Sam Edelman brown - - 186.25 - - 186.25 - product - - - - The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. - 578902-00 - delivery - - - - - - - - - - - - - - - - - - - - - - - - - - - 578902-00 - 186.25 - - - 578902-00 - 186.25 - - - diff --git a/graphql_vuestorefront/data/mail_template.xml b/graphql_vuestorefront/data/mail_template.xml deleted file mode 100644 index c416352..0000000 --- a/graphql_vuestorefront/data/mail_template.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - Website Reset Password - - Password reset - "{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}> - {{ object.email_formatted }} - - - - - - - - - - -
- - - - - - - - - - - - - - - - -
- - - - - - - - -
- Your Account -
- - Marc Demo - -
- -
-
-
-
- - - - - - - -
-
- Dear Marc Demo, -
-
- A password reset was requested for the Odoo account linked to this email. - You may change your password by following this link which will remain valid during 24 hours:
- - If you do not expect this, you can safely ignore this email. -
-
- Thanks, - -
- --
Mitchell Admin
-
-
-
-
-
-
- - - - - - - -
- YourCompany -
- +1 650-123-4567 - - | - - info@yourcompany.com - - - - | - - http://www.example.com - - -
-
-
- - - - -
- Powered by - Odoo - -
-
-
- {{ object.lang }} - -
-
-
diff --git a/graphql_vuestorefront/models/__init__.py b/graphql_vuestorefront/models/__init__.py deleted file mode 100644 index 4b9a538..0000000 --- a/graphql_vuestorefront/models/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -from . import invalidate_cache -from . import ir_http -from . import website -from . import product -from . import res_config_settings -from . import res_users -from . import payment_transaction diff --git a/graphql_vuestorefront/models/invalidate_cache.py b/graphql_vuestorefront/models/invalidate_cache.py deleted file mode 100644 index 4fd180e..0000000 --- a/graphql_vuestorefront/models/invalidate_cache.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import logging -from datetime import datetime - -import requests -from odoo import models, fields, api - -_logger = logging.getLogger(__name__) - - -class InvalidateCache(models.Model): - _name = 'invalidate.cache' - _description = 'VSF Invalidate Cache' - - res_model = fields.Char('Res Model', required=True, index=True) - res_id = fields.Integer('Res ID', required=True) - - def init(self): - super().init() - self.env.cr.execute(""" - CREATE INDEX IF NOT EXISTS invalidate_cache_find_idx - ON invalidate_cache(res_model, res_id); - """) - - @api.model - def find_invalidate_cache(self, res_model, res_id): - cr = self.env.cr - query = """ - SELECT id - FROM invalidate_cache - WHERE res_model=%s AND res_id=%s - LIMIT 1; - """ - params = (res_model, res_id,) - - cr.execute(query, params) - return cr.fetchone() - - @api.model - def create_invalidate_cache(self, res_model, res_ids): - ICP = self.env['ir.config_parameter'].sudo() - cache_invalidation_enable = ICP.get_param('vsf_cache_invalidation', False) - - if not cache_invalidation_enable: - return False - - for res_id in res_ids: - if not self.find_invalidate_cache(res_model, res_id): - query = """ - INSERT INTO invalidate_cache(res_model, res_id, create_date, write_date, create_uid, write_uid) - VALUES(%s, %s, %s, %s, %s, %s); - """ - now = datetime.now() - uid = self.env.user.id - params = (res_model, res_id, now, now, uid, uid,) - - self.env.cr.execute(query, params) - - @api.model - def delete_invalidate_cache(self, ids): - if len(ids) == 1: - ids = '({})'.format(ids[0]) - else: - ids = tuple(ids) - - query = """ - DELETE FROM invalidate_cache - WHERE id IN {}; - """.format(ids) - - self.env.cr.execute(query) - - @api.model - def request_cache_invalidation(self, url, key, tags): - if url and key and tags: - try: - requests.get(url, params={'key': key, 'tags': tags}, timeout=5) - except Exception as e: - _logger.error(e) - self.env.cr.rollback() - - @api.model - def request_vsf_cache_invalidation(self): - ICP = self.env['ir.config_parameter'].sudo() - url = ICP.get_param('vsf_cache_invalidation_url', False) - key = ICP.get_param('vsf_cache_invalidation_key', False) - - models = [ - { - 'name': 'product.template', - 'tags_method': '_get_product_tags', - }, - { - 'name': 'product.public.category', - 'tags_method': '_get_category_tags', - }, - ] - - for model in models: - invalidate_caches = self.env['invalidate.cache'].search([('res_model', '=', model['name'])]) - if invalidate_caches: - res_ids = invalidate_caches.mapped('res_id') - tags = getattr(self, model['tags_method'])(res_ids) - self.delete_invalidate_cache(invalidate_caches.ids) - self.request_cache_invalidation(url, key, tags) - self.env.cr.commit() - - def _get_product_tags(self, product_ids): - tags = ','.join(f'P{product_id}' for product_id in product_ids) - category_ids = self.env['product.template'].search( - [('id', 'in', product_ids)]).mapped('public_categ_slug_ids').ids - if category_ids: - tags += ',' + ','.join(f'C{category_id}' for category_id in category_ids) - return tags - - def _get_category_tags(self, product_ids): - tags = ','.join(f'P{product_id}' for product_id in product_ids) - category_ids = self.env['product.template'].search( - [('id', 'in', product_ids)]).mapped('public_categ_slug_ids').ids - if category_ids: - tags += ',' + ','.join(f'C{category_id}' for category_id in category_ids) - return tags diff --git a/graphql_vuestorefront/models/ir_http.py b/graphql_vuestorefront/models/ir_http.py deleted file mode 100644 index 2d04da7..0000000 --- a/graphql_vuestorefront/models/ir_http.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import base64 -import codecs -import io - -from PIL.WebPImagePlugin import Image -from odoo import api, http, models -from odoo.http import request -from odoo.tools import image_process -from odoo.tools.safe_eval import safe_eval - - -class Http(models.AbstractModel): - _inherit = 'ir.http' - - @api.model - def _content_image(self, xmlid=None, model='ir.attachment', res_id=None, field='datas', - filename_field='name', unique=None, filename=None, mimetype=None, download=None, - width=0, height=0, crop=False, quality=0, access_token=None, **kwargs): - if filename and filename.endswith(('jpeg', 'jpg')): - request.image_format = 'jpeg' - - return super(Http, self)._content_image(xmlid=xmlid, model=model, res_id=res_id, field=field, - filename_field=filename_field, unique=unique, filename=filename, - mimetype=mimetype, download=download, width=width, height=height, - crop=crop, quality=quality, access_token=access_token, **kwargs) - - @api.model - def _content_image_get_response(self, status, headers, image_base64, model='ir.attachment', - field='datas', download=None, width=0, height=0, crop=False, quality=0): - """ Center image in background with color, resize, compress and convert image to webp or jpeg """ - if status == 200 and image_base64 and width and height: - try: - # Accepts jpeg and webp, defaults to webp if none found - if hasattr(request, 'image_format'): - image_format = request.image_format - else: - image_format = 'webp' - - width = int(width) - height = int(height) - ICP = request.env['ir.config_parameter'].sudo() - - image_base64 = image_process(image_base64, size=(width, height)) - img = Image.open(io.BytesIO(codecs.decode(image_base64, 'base64'))) - if img.mode != 'RGBA': - img = img.convert('RGBA') - - # Get background color from settings - try: - background_rgba = safe_eval(ICP.get_param('vsf_image_background_rgba', '(255, 255, 255, 255)')) - except: - background_rgba = (255, 255, 255, 255) - - # Create a new background, merge the background with the image centered - img_w, img_h = img.size - if image_format == 'jpeg': - background = Image.new('RGB', (width, height), background_rgba[:3]) - else: - background = Image.new('RGBA', (width, height), background_rgba) - bg_w, bg_h = background.size - offset = ((bg_w - img_w) // 2, (bg_h - img_h) // 2) - background.paste(img, offset) - - # Get compression quality from settings - quality = ICP.get_param('vsf_image_quality', 100) - - stream = io.BytesIO() - if image_format == 'jpeg': - background.save(stream, format=image_format.upper(), subsampling=0) - else: - background.save(stream, format=image_format.upper(), quality=quality, subsampling=0) - image_base64 = base64.b64encode(stream.getvalue()) - - except Exception: - return request.not_found() - - # Replace Content-Type by generating a new list of headers - new_headers = [] - for index, header in enumerate(headers): - if header[0] == 'Content-Type': - new_headers.append(('Content-Type', f'image/{image_format}')) - else: - new_headers.append(header) - - # Response - content = base64.b64decode(image_base64) - new_headers = http.set_safe_image_headers(new_headers, content) - response = request.make_response(content, new_headers) - response.status_code = status - return response - - # Fallback to super function - return super(Http, self)._content_image_get_response( - status, headers, image_base64, model=model, field=field, download=download, width=width, height=height, - crop=crop, quality=quality) diff --git a/graphql_vuestorefront/models/product.py b/graphql_vuestorefront/models/product.py deleted file mode 100644 index 526110a..0000000 --- a/graphql_vuestorefront/models/product.py +++ /dev/null @@ -1,266 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import json -import requests -from odoo import models, fields, api, tools, _ -from odoo.addons.http_routing.models.ir_http import slug, slugify -from odoo.exceptions import ValidationError - - -class ProductTemplate(models.Model): - _inherit = 'product.template' - - def _get_public_categ_slug(self, category_ids, category): - category_ids.append(category.id) - - if category.parent_id: - category_ids = self._get_public_categ_slug(category_ids, category.parent_id) - - return category_ids - - @api.depends('public_categ_ids') - def _compute_public_categ_slug_ids(self): - """ To allow search of website_slug on parent categories """ - cr = self.env.cr - - for product in self: - category_ids = [] - - for category in product.public_categ_ids: - category_ids = product._get_public_categ_slug(category_ids, category) - - cr.execute(""" - DELETE FROM product_template_product_public_category_slug_rel - WHERE product_template_id=%s; - """, (product.id,)) - - for category_id in list(dict.fromkeys(category_ids)): - cr.execute(""" - INSERT INTO product_template_product_public_category_slug_rel(product_template_id, product_public_category_id) - VALUES(%s, %s); - """, (product.id, category_id,)) - - @api.depends('name') - def _compute_website_slug(self): - langs = self.env['res.lang'].search([]) - - for product in self: - for lang in langs: - product = product.with_context(lang=lang.code) - - if not product.id: - product.website_slug = None - else: - prefix = '/product' - slug_name = slugify(product.name or '').strip().strip('-') - product.website_slug = '{}/{}-{}'.format(prefix, slug_name, product.id) - - @api.depends('product_variant_ids') - def _compute_variant_attribute_value_ids(self): - """ - Used to filter attribute values on the website. - This method computes a list of attribute values from variants of published products. - This will ensure that the available attribute values on the website filtering will return results. - By default, Odoo only shows attributes that will return results but doesn't consider that a particular - attribute value may not have a variant. - """ - for product in self: - variants = product.product_variant_ids - attribute_values = variants.\ - mapped('product_template_attribute_value_ids').\ - mapped('product_attribute_value_id') - product.variant_attribute_value_ids = [(6, 0, attribute_values.ids)] - - variant_attribute_value_ids = fields.Many2many('product.attribute.value', - 'product_template_variant_product_attribute_value_rel', - compute='_compute_variant_attribute_value_ids', - store=True, readonly=True) - website_slug = fields.Char('Website Slug', compute='_compute_website_slug', store=True, readonly=True, - translate=True) - public_categ_slug_ids = fields.Many2many('product.public.category', - 'product_template_product_public_category_slug_rel', - compute='_compute_public_categ_slug_ids', - store=True, readonly=True) - json_ld = fields.Char('JSON-LD') - - def write(self, vals): - res = super(ProductTemplate, self).write(vals) - self.env['invalidate.cache'].create_invalidate_cache(self._name, self.ids) - return res - - def unlink(self): - self.env['invalidate.cache'].create_invalidate_cache(self._name, self.ids) - return super(ProductTemplate, self).unlink() - - def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, - parent_combination=False, only_template=False): - """ Add discount value and percentage based """ - combination_info = super(ProductTemplate, self)._get_combination_info( - combination=combination, product_id=product_id, add_qty=add_qty, pricelist=pricelist, - parent_combination=parent_combination, only_template=only_template) - - discount = 0.00 - discount_perc = 0 - if combination_info['has_discounted_price'] and product_id: - discount = combination_info['list_price'] - combination_info['price'] - discount_perc = combination_info['list_price'] and (discount * 100 / combination_info['list_price']) or 0 - - if discount_perc: - discount_perc = int(round(discount_perc, 0)) - if not discount_perc: - discount_perc = 1 - - combination_info.update({ - 'discount': round(discount, 2), - 'discount_perc': discount_perc, - }) - - return combination_info - - def get_json_ld(self): - self.ensure_one() - if self.json_ld: - return self.json_ld - - env = self.env - website = env['website'].get_current_website() - base_url = env['ir.config_parameter'].sudo().get_param('web.base.url', '') - if base_url and base_url[-1:] == '/': - base_url = base_url[:-1] - - # Get list of images - images = list() - if self.image_1920: - images.append('%s/web/image/product.product/%s/image' % (base_url, self.id)) - - json_ld = { - "@context": "https://schema.org/", - "@type": "Product", - "name": self.display_name, - "image": images, - "offers": { - "@type": "Offer", - "url": "%s/product/%s" % (website.domain or '', slug(self)), - "priceCurrency": self.currency_id.name, - "price": self.list_price, - "itemCondition": "https://schema.org/NewCondition", - "availability": "https://schema.org/InStock", - "seller": { - "@type": "Organization", - "name": website and website.display_name or self.env.user.company_id.display_name - } - } - } - - if self.description_sale: - json_ld.update({"description": self.description_sale}) - - if self.default_code: - json_ld.update({"sku": self.default_code}) - - return json.dumps(json_ld) - - -class ProductProduct(models.Model): - _inherit = 'product.product' - - def get_json_ld(self): - self.ensure_one() - if self.json_ld: - return self.json_ld - - env = self.env - website = env['website'].get_current_website() - base_url = env['ir.config_parameter'].sudo().get_param('web.base.url', '') - if base_url and base_url[-1:] == '/': - base_url = base_url[:-1] - - # Get list of images - images = list() - if self.image_1920: - images.append('%s/web/image/product.product/%s/image' % (base_url, self.id)) - - json_ld = { - "@context": "https://schema.org/", - "@type": "Product", - "name": self.display_name, - "image": images, - "offers": { - "@type": "Offer", - "url": "%s/product/%s" % (website.domain or '', slug(self)), - "priceCurrency": self.currency_id.name, - "price": self.list_price, - "itemCondition": "https://schema.org/NewCondition", - "availability": "https://schema.org/InStock", - "seller": { - "@type": "Organization", - "name": website and website.display_name or self.env.user.company_id.display_name - } - } - } - - if self.description_sale: - json_ld.update({"description": self.description_sale}) - - if self.default_code: - json_ld.update({"sku": self.default_code}) - - return json.dumps(json_ld) - - -class ProductPublicCategory(models.Model): - _inherit = 'product.public.category' - - def _validate_website_slug(self): - for category in self.filtered(lambda c: c.website_slug): - if category.website_slug[0] != '/': - raise ValidationError(_('Slug should start with /')) - - if self.search([('website_slug', '=', category.website_slug), ('id', '!=', category.id)], limit=1): - raise ValidationError(_('Slug is already in use: {}'.format(category.website_slug))) - - website_slug = fields.Char('Website Slug', translate=True, copy=False) - json_ld = fields.Char('JSON-LD') - - @api.model - def create(self, vals): - rec = super(ProductPublicCategory, self).create(vals) - - if rec.website_slug: - rec._validate_website_slug() - else: - rec.website_slug = '/category/{}'.format(rec.id) - - return rec - - def write(self, vals): - res = super(ProductPublicCategory, self).write(vals) - if vals.get('website_slug', False): - self._validate_website_slug() - self.env['invalidate.cache'].create_invalidate_cache(self._name, self.ids) - return res - - def unlink(self): - self.env['invalidate.cache'].create_invalidate_cache(self._name, self.ids) - return super(ProductPublicCategory, self).unlink() - - def get_json_ld(self): - self.ensure_one() - if self.json_ld: - return self.json_ld - - website = self.env['website'].get_current_website() - base_url = website.domain or '' - if base_url and base_url[-1] == '/': - base_url = base_url[:-1] - - json_ld = { - "@context": "https://schema.org", - "@type": "CollectionPage", - "url": '{}{}'.format(base_url, self.website_slug or ''), - "name": self.display_name, - } - - return json.dumps(json_ld) diff --git a/graphql_vuestorefront/models/res_config_settings.py b/graphql_vuestorefront/models/res_config_settings.py deleted file mode 100644 index 9a12931..0000000 --- a/graphql_vuestorefront/models/res_config_settings.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import uuid -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError -from odoo.tools.safe_eval import safe_eval - - -class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' - - vsf_debug_mode = fields.Boolean('Debug Mode') - vsf_payment_success_return_url = fields.Char( - 'Payment Success Return Url', related='website_id.vsf_payment_success_return_url', readonly=False, - required=True - ) - vsf_payment_error_return_url = fields.Char( - 'Payment Error Return Url', related='website_id.vsf_payment_error_return_url', readonly=False, - required=True - ) - vsf_cache_invalidation = fields.Boolean('Cache Invalidation') - vsf_cache_invalidation_key = fields.Char('Cache Invalidation Key', required=True) - vsf_cache_invalidation_url = fields.Char('Cache Invalidation Url', required=True) - vsf_mailing_list_id = fields.Many2one('mailing.list', 'Newsletter', domain=[('is_public', '=', True)], - related='website_id.vsf_mailing_list_id', readonly=False, required=True) - - # VSF Images - vsf_image_quality = fields.Integer('Quality', required=True) - vsf_image_background_rgba = fields.Char('Background RGBA', required=True) - vsf_image_resize_limit = fields.Integer('Resize Limit', required=True, - help='Limit in pixels to resize image for width and height') - - def get_values(self): - res = super(ResConfigSettings, self).get_values() - ICP = self.env['ir.config_parameter'].sudo() - res.update( - vsf_debug_mode=ICP.get_param('vsf_debug_mode'), - vsf_cache_invalidation=ICP.get_param('vsf_cache_invalidation'), - vsf_cache_invalidation_key=ICP.get_param('vsf_cache_invalidation_key'), - vsf_cache_invalidation_url=ICP.get_param('vsf_cache_invalidation_url'), - vsf_image_quality=int(ICP.get_param('vsf_image_quality', 100)), - vsf_image_background_rgba=ICP.get_param('vsf_image_background_rgba', '(255, 255, 255, 255)'), - vsf_image_resize_limit=int(ICP.get_param('vsf_image_resize_limit', 1920)), - ) - return res - - def set_values(self): - if self.vsf_image_quality < 0 or self.vsf_image_quality > 100: - raise ValidationError(_('Invalid image quality percentage.')) - - if self.vsf_image_resize_limit < 0: - raise ValidationError(_('Invalid image resize limit.')) - - super(ResConfigSettings, self).set_values() - ICP = self.env['ir.config_parameter'].sudo() - ICP.set_param('vsf_debug_mode', self.vsf_debug_mode) - ICP.set_param('vsf_cache_invalidation', self.vsf_cache_invalidation) - ICP.set_param('vsf_cache_invalidation_key', self.vsf_cache_invalidation_key) - ICP.set_param('vsf_cache_invalidation_url', self.vsf_cache_invalidation_url) - ICP.set_param('vsf_image_quality', self.vsf_image_quality) - ICP.set_param('vsf_image_background_rgba', self.vsf_image_background_rgba) - ICP.set_param('vsf_image_resize_limit', self.vsf_image_resize_limit) - - @api.model - def create_vsf_cache_invalidation_key(self): - ICP = self.env['ir.config_parameter'].sudo() - ICP.set_param('vsf_cache_invalidation_key', str(uuid.uuid4())) diff --git a/graphql_vuestorefront/models/res_users.py b/graphql_vuestorefront/models/res_users.py deleted file mode 100644 index db4434a..0000000 --- a/graphql_vuestorefront/models/res_users.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import logging - -from odoo.http import request -from odoo import api, models, _ -from odoo.addons.auth_signup.models.res_partner import now -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - - -class ResUsers(models.Model): - _inherit = 'res.users' - - def api_action_reset_password(self): - """ create signup token for each user, and send their signup url by email """ - if self.filtered(lambda user: not user.active): - raise UserError(_("You cannot perform this action on an archived user.")) - # prepare reset password signup - create_mode = bool(self.env.context.get('create_user')) - - # no time limit for initial invitation, only for reset password - expiration = False if create_mode else now(days=+1) - - self.mapped('partner_id').signup_prepare(signup_type="reset", expiration=expiration) - - # send email to users with their signup url - template = self.env.ref('graphql_vuestorefront.website_reset_password_email') - - assert template._name == 'mail.template' - - website = request.env['website'].get_current_website() - domain = website.domain or '' - if domain and domain[-1] == '/': - domain = domain[:-1] - - email_values = { - 'email_cc': False, - 'auto_delete': True, - 'recipient_ids': [], - 'partner_ids': [], - 'scheduled_date': False, - } - - for user in self: - token = user.signup_token - signup_url = "%s/forgot-password/new-password?token=%s" % (domain, token) - if not user.email: - raise UserError(_("Cannot send email: user %s has no email address.", user.name)) - email_values['email_to'] = user.email - with self.env.cr.savepoint(): - force_send = not create_mode - template.with_context(lang=user.lang, signup_url=signup_url).send_mail( - user.id, force_send=force_send, raise_exception=True, email_values=email_values) - _logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email) diff --git a/graphql_vuestorefront/models/website.py b/graphql_vuestorefront/models/website.py deleted file mode 100644 index c7d3b73..0000000 --- a/graphql_vuestorefront/models/website.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import requests -from odoo import models, fields, api - - -class Website(models.Model): - _inherit = 'website' - - vsf_payment_success_return_url = fields.Char( - 'Payment Success Return Url', required=True, translate=True, default='Dummy' - ) - vsf_payment_error_return_url = fields.Char( - 'Payment Error Return Url', required=True, translate=True, default='Dummy' - ) - vsf_mailing_list_id = fields.Many2one('mailing.list', 'Newsletter', domain=[('is_public', '=', True)]) - - @api.model - def enable_b2c_reset_password(self): - """ Enable sign up and reset password on default website """ - website = self.env.ref('website.default_website', raise_if_not_found=False) - if website: - website.auth_signup_uninvited = 'b2c' - - ICP = self.env['ir.config_parameter'].sudo() - ICP.set_param('auth_signup.invitation_scope', 'b2c') - ICP.set_param('auth_signup.reset_password', True) - - -class WebsiteRewrite(models.Model): - _inherit = 'website.rewrite' - - def _get_vsf_tags(self): - tags = 'WR%s' % self.id - return tags - - def _vsf_request_cache_invalidation(self): - ICP = self.env['ir.config_parameter'].sudo() - url = ICP.get_param('vsf_cache_invalidation_url', False) - key = ICP.get_param('vsf_cache_invalidation_key', False) - - if url and key: - try: - for website_rewrite in self: - tags = website_rewrite._get_vsf_tags() - - # Make the GET request to the /cache-invalidate - requests.get(url, params={'key': key, 'tags': tags}, timeout=5) - except: - pass - - def write(self, vals): - res = super(WebsiteRewrite, self).write(vals) - self._vsf_request_cache_invalidation() - return res - - def unlink(self): - self._vsf_request_cache_invalidation() - return super(WebsiteRewrite, self).unlink() - - -class WebsiteMenu(models.Model): - _inherit = 'website.menu' - - is_footer = fields.Boolean('Is Footer', default=False) - menu_image_ids = fields.One2many('website.menu.image', 'menu_id', string='Menu Images') - is_mega_menu = fields.Boolean(store=True) - - -class WebsiteMenuImage(models.Model): - _name = 'website.menu.image' - _description = 'Website Menu Image' - - def _default_sequence(self): - menu = self.search([], limit=1, order="sequence DESC") - return menu.sequence or 0 - - menu_id = fields.Many2one('website.menu', 'Website Menu', required=True) - sequence = fields.Integer(default=_default_sequence) - image = fields.Image(string='Image', required=True) - tag = fields.Char('Tag') - title = fields.Char('Title') - subtitle = fields.Char('Subtitle') - text_color = fields.Char('Text Color (Hex)', help='#111000') - button_text = fields.Char('Button Text') - button_url = fields.Char('Button URL') diff --git a/graphql_vuestorefront/schemas/product.py b/graphql_vuestorefront/schemas/product.py deleted file mode 100644 index 921717e..0000000 --- a/graphql_vuestorefront/schemas/product.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import graphene -from graphql import GraphQLError -from odoo.http import request -from odoo import _ -from odoo.osv import expression - -from odoo.addons.graphql_vuestorefront.schemas.objects import ( - SortEnum, Product, Attribute, AttributeValue -) - - -def get_search_order(sort): - sorting = '' - for field, val in sort.items(): - if sorting: - sorting += ', ' - if field == 'price': - sorting += 'list_price %s' % val.value - else: - sorting += '%s %s' % (field, val.value) - - # Add id as last factor, so we can consistently get the same results - if sorting: - sorting += ', id ASC' - else: - sorting = 'id ASC' - - return sorting - - -def get_search_domain(env, search, **kwargs): - # Only get published products - domains = [env['website'].get_current_website().sale_product_domain()] - - # Filter with ids - if kwargs.get('ids', False): - domains.append([('id', 'in', kwargs['ids'])]) - - # Filter with Category ID - if kwargs.get('category_id', False): - domains.append([('public_categ_ids', 'child_of', kwargs['category_id'])]) - - # Filter with Category Slug - if kwargs.get('category_slug', False): - domains.append([('public_categ_slug_ids.website_slug', '=', kwargs['category_slug'])]) - - # Filter With Name - if kwargs.get('name', False): - name = kwargs['name'] - for n in name.split(" "): - domains.append([('name', 'ilike', n)]) - - if search: - for srch in search.split(" "): - domains.append([ - '|', '|', ('name', 'ilike', srch), ('description_sale', 'like', srch), ('default_code', 'like', srch)]) - - partial_domain = domains.copy() - - # Product Price Filter - if kwargs.get('min_price', False): - domains.append([('list_price', '>=', float(kwargs['min_price']))]) - if kwargs.get('max_price', False): - domains.append([('list_price', '<=', float(kwargs['max_price']))]) - - # Deprecated: filter with Attribute Value - if kwargs.get('attribute_value_id', False): - domains.append([('attribute_line_ids.value_ids', 'in', kwargs['attribute_value_id'])]) - - # Filter with Attribute Value - if kwargs.get('attrib_values', False): - attributes = {} - attributes_domain = [] - - for value in kwargs['attrib_values']: - try: - value = value.split('-') - if len(value) != 2: - continue - - attribute_id = int(value[0]) - attribute_value_id = int(value[1]) - except ValueError: - continue - - if attribute_id not in attributes: - attributes[attribute_id] = [] - - attributes[attribute_id].append(attribute_value_id) - - for key, value in attributes.items(): - attributes_domain.append([('attribute_line_ids.value_ids', 'in', value)]) - - attributes_domain = expression.AND(attributes_domain) - domains.append(attributes_domain) - - return expression.AND(domains), expression.AND(partial_domain) - - -def get_product_list(env, current_page, page_size, search, sort, **kwargs): - Product = env['product.template'].sudo() - domain, partial_domain = get_search_domain(env, search, **kwargs) - - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 - order = get_search_order(sort) - products = Product.search(domain, order=order) - - # If attribute values are selected, we need to get the full list of attribute values and prices - if domain == partial_domain: - attribute_values = products.mapped('variant_attribute_value_ids') - prices = products.mapped('list_price') - else: - without_attributes_products = Product.search(partial_domain) - attribute_values = without_attributes_products.mapped('variant_attribute_value_ids') - prices = without_attributes_products.mapped('list_price') - - total_count = len(products) - products = products[offset:offset + page_size] - if prices: - return products, total_count, attribute_values, min(prices), max(prices) - return products, total_count, attribute_values, 0.0, 0.0 - - -class Products(graphene.Interface): - products = graphene.List(Product) - total_count = graphene.Int(required=True) - attribute_values = graphene.List(AttributeValue) - min_price = graphene.Float() - max_price = graphene.Float() - - -class ProductList(graphene.ObjectType): - class Meta: - interfaces = (Products,) - - -class ProductFilterInput(graphene.InputObjectType): - ids = graphene.List(graphene.Int) - category_id = graphene.List(graphene.Int) - category_slug = graphene.String() - # Deprecated - attribute_value_id = graphene.List(graphene.Int) - attrib_values = graphene.List(graphene.String) - name = graphene.String() - min_price = graphene.Float() - max_price = graphene.Float() - - -class ProductSortInput(graphene.InputObjectType): - id = SortEnum() - name = SortEnum() - price = SortEnum() - - -class ProductVariant(graphene.Interface): - product = graphene.Field(Product) - product_template_id = graphene.Int() - display_name = graphene.String() - display_image = graphene.Boolean() - price = graphene.Float() - list_price = graphene.String() - has_discounted_price = graphene.Boolean() - is_combination_possible = graphene.Boolean() - - -class ProductVariantData(graphene.ObjectType): - class Meta: - interfaces = (ProductVariant,) - - -class ProductQuery(graphene.ObjectType): - product = graphene.Field( - Product, - id=graphene.Int(default_value=None), - slug=graphene.String(default_value=None), - barcode=graphene.String(default_value=None), - ) - products = graphene.Field( - Products, - filter=graphene.Argument(ProductFilterInput, default_value={}), - current_page=graphene.Int(default_value=1), - page_size=graphene.Int(default_value=20), - search=graphene.String(default_value=False), - sort=graphene.Argument(ProductSortInput, default_value={}) - ) - attribute = graphene.Field( - Attribute, - required=True, - id=graphene.Int(), - ) - product_variant = graphene.Field( - ProductVariant, - required=True, - product_template_id=graphene.Int(), - combination_id=graphene.List(graphene.Int) - ) - - @staticmethod - def resolve_product(self, info, id=None, slug=None, barcode=None): - env = info.context["env"] - Product = env["product.template"].sudo() - - if id: - product = Product.search([('id', '=', id)], limit=1) - elif slug: - product = Product.search([('website_slug', '=', slug)], limit=1) - elif barcode: - product = Product.search([('barcode', '=', barcode)], limit=1) - else: - product = Product - - if product: - website = env['website'].get_current_website() - request.website = website - if not product.can_access_from_current_website(): - product = Product - - return product - - @staticmethod - def resolve_products(self, info, filter, current_page, page_size, search, sort): - env = info.context["env"] - products, total_count, attribute_values,min_price, max_price = get_product_list( - env, current_page, page_size, search, sort, **filter) - return ProductList(products=products, total_count=total_count, attribute_values=attribute_values, - min_price=min_price, max_price=max_price) - - @staticmethod - def resolve_attribute(self, info, id): - return info.context["env"]["product.attribute"].search([('id', '=', id)], limit=1) - - @staticmethod - def resolve_product_variant(self, info, product_template_id, combination_id): - env = info.context["env"] - - website = env['website'].get_current_website() - request.website = website - pricelist = website.get_current_pricelist() - - product_template = env['product.template'].browse(product_template_id) - combination = env['product.template.attribute.value'].browse(combination_id) - - variant_info = product_template._get_combination_info(combination, pricelist) - - product = env['product.product'].browse(variant_info['product_id']) - - # Condition to verify if Product exist - if not product: - raise GraphQLError(_('Product does not exist')) - - is_combination_possible = product_template._is_combination_possible(combination) - - # Condition to Verify if Product is active or if combination exist - if not product.active or not is_combination_possible: - variant_info['is_combination_possible'] = False - else: - variant_info['is_combination_possible'] = True - - return ProductVariantData( - product=product, - product_template_id=variant_info['product_template_id'], - display_name=variant_info['display_name'], - display_image=variant_info['display_image'], - price=variant_info['price'], - list_price=variant_info['list_price'], - has_discounted_price=variant_info['has_discounted_price'], - is_combination_possible=variant_info['is_combination_possible'] - ) diff --git a/graphql_vuestorefront/schemas/website.py b/graphql_vuestorefront/schemas/website.py deleted file mode 100644 index 07a3e39..0000000 --- a/graphql_vuestorefront/schemas/website.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -import graphene - -from odoo.addons.graphql_vuestorefront.schemas.objects import WebsiteMenu - - -class WebsiteQuery(graphene.ObjectType): - website_menu = graphene.List( - graphene.NonNull(WebsiteMenu), - ) - website_mega_menu = graphene.List( - graphene.NonNull(WebsiteMenu), - ) - website_footer = graphene.List( - graphene.NonNull(WebsiteMenu), - ) - - @staticmethod - def resolve_website_menu(self, info): - env = info.context['env'] - website = env['website'].get_current_website() - - domain = [ - ('website_id', '=', website.id), - ('is_visible', '=', True), - ('is_footer', '=', False), - ('is_mega_menu', '=', False), - ] - - return env['website.menu'].search(domain) - - @staticmethod - def resolve_website_mega_menu(self, info): - env = info.context['env'] - website = env['website'].get_current_website() - - domain = [ - ('website_id', '=', website.id), - ('is_visible', '=', True), - ('is_footer', '=', False), - ('is_mega_menu', '=', True), - ] - - return env['website.menu'].search(domain) - - @staticmethod - def resolve_website_footer(self, info): - env = info.context['env'] - website = env['website'].get_current_website() - - domain = [ - ('website_id', '=', website.id), - ('is_visible', '=', True), - ('is_footer', '=', True), - ('is_mega_menu', '=', False), - ] - - return env['website.menu'].search(domain) diff --git a/graphql_vuestorefront/security/ir.model.access.csv b/graphql_vuestorefront/security/ir.model.access.csv deleted file mode 100644 index 3fd1e22..0000000 --- a/graphql_vuestorefront/security/ir.model.access.csv +++ /dev/null @@ -1,4 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -graphql_vuestorefront.access_invalidate_cache,access_invalidate_cache,graphql_vuestorefront.model_invalidate_cache,base.group_user,1,1,1,1 -access_website_menu_image,access_website_menu_image,model_website_menu_image,,1,0,0,0 -access_website_menu_image_designer,access_website_menu_image_designer,graphql_vuestorefront.model_website_menu_image,website.group_website_designer,1,1,1,1 \ No newline at end of file diff --git a/payment_adyen_vsf/__init__.py b/payment_adyen_vsf/__init__.py index 0405454..1ee9576 100644 --- a/payment_adyen_vsf/__init__.py +++ b/payment_adyen_vsf/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/payment_adyen_vsf/__manifest__.py b/payment_adyen_vsf/__manifest__.py index 668f150..88da8af 100644 --- a/payment_adyen_vsf/__manifest__.py +++ b/payment_adyen_vsf/__manifest__.py @@ -1,31 +1,23 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). { # Application Information - 'name': 'Adyen Payment Acquirer to VSF', - 'category': 'Accounting/Payment Acquirers', - 'version': '16.0.1.0.1', - 'summary': 'Adyen Payment Acquirer: Adapting Adyen to VSF', - + "name": "Adyen Payment Acquirer to VSF", + "category": "Accounting/Payment Acquirers", + "version": "16.0.1.0.1", + "summary": "Adyen Payment Acquirer: Adapting Adyen to VSF", # Author - 'author': "OdooGap", - 'website': "https://www.odoogap.com/", - 'maintainer': 'OdooGap', - 'license': 'LGPL-3', - + "author": "OdooGap", + "website": "https://www.odoogap.com/", + "maintainer": "OdooGap", + "license": "LGPL-3", # Dependencies - 'depends': [ - 'payment', - 'payment_adyen' - ], - + "depends": ["payment", "payment_adyen"], # Views - 'data': [], - + "data": [], # Technical - 'installable': True, - 'application': False, - 'auto_install': False, + "installable": True, + "application": False, + "auto_install": False, } diff --git a/payment_adyen_vsf/const.py b/payment_adyen_vsf/const.py index b6c8b53..47c1e66 100644 --- a/payment_adyen_vsf/const.py +++ b/payment_adyen_vsf/const.py @@ -1,41 +1,46 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.addons.payment_adyen.const import API_ENDPOINT_VERSIONS, CURRENCY_DECIMALS, RESULT_CODES_MAPPING +from odoo.addons.payment_adyen.const import API_ENDPOINT_VERSIONS, CURRENCY_DECIMALS # Endpoints of the API. -# See https://docs.adyen.com/api-explorer/#/CheckoutService/v67/overview for Checkout API -# See https://docs.adyen.com/api-explorer/#/Recurring/v49/overview for Recurring API -API_ENDPOINT_VERSIONS.update({ - '/payments/{}/reversals': 67, # Checkout API -}) +# See for Checkout API: +# https://docs.adyen.com/api-explorer/#/CheckoutService/v67/overview +# See for Recurring API: +# https://docs.adyen.com/api-explorer/#/Recurring/v49/overview +API_ENDPOINT_VERSIONS.update( + { + "/payments/{}/reversals": 67, # Checkout API + } +) # Adyen-specific mapping of currency codes in ISO 4217 format to the number of decimals. # Only currencies for which Adyen does not follow the ISO 4217 norm are listed here. # See https://docs.adyen.com/development-resources/currency-codes -CURRENCY_DECIMALS.update({ - "BHD": 3, - "DJF": 0, - "GNF": 0, - "JOD": 3, - "JPY": 0, - "KMF": 0, - "KRW": 0, - "KWD": 3, - "LYD": 3, - "OMR": 3, - "PYG": 0, - "RWF": 0, - "TND": 3, - "UGX": 0, - "VND": 0, - "VUV": 0, - "XAF": 0, - "XOF": 0, - "XPF": 0, - "USD": 2, - "EUR": 2, - "SEK": 2, - "DKK": 2, -}) +CURRENCY_DECIMALS.update( + { + "BHD": 3, + "DJF": 0, + "GNF": 0, + "JOD": 3, + "JPY": 0, + "KMF": 0, + "KRW": 0, + "KWD": 3, + "LYD": 3, + "OMR": 3, + "PYG": 0, + "RWF": 0, + "TND": 3, + "UGX": 0, + "VND": 0, + "VUV": 0, + "XAF": 0, + "XOF": 0, + "XPF": 0, + "USD": 2, + "EUR": 2, + "SEK": 2, + "DKK": 2, + } +) diff --git a/payment_adyen_vsf/controllers/__init__.py b/payment_adyen_vsf/controllers/__init__.py index 0aec262..cb3a0ef 100644 --- a/payment_adyen_vsf/controllers/__init__.py +++ b/payment_adyen_vsf/controllers/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/payment_adyen_vsf/controllers/apple_pay.py b/payment_adyen_vsf/controllers/apple_pay.py index 5f5d208..4affecb 100644 --- a/payment_adyen_vsf/controllers/apple_pay.py +++ b/payment_adyen_vsf/controllers/apple_pay.py @@ -1,25 +1,34 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import os + from odoo import http from odoo.http import request -import os class AppleMerchantIDController(http.Controller): - - @http.route('/.well-known/apple-developer-merchantid-domain-association', type='http', auth='public') + @http.route( + "/.well-known/apple-developer-merchantid-domain-association", + type="http", + auth="public", + ) def apple_merchant_id(self, **kw): - file_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..', 'static', 'description', 'apple-developer-merchantid-domain-association') + file_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "static", + "description", + "apple-developer-merchantid-domain-association", + ) ) try: - with open(file_path, 'r') as file: + with open(file_path) as file: file_content = file.read() - headers = [('Content-Type', 'text/plain')] + headers = [("Content-Type", "text/plain")] return request.make_response(file_content, headers) # Return error 404 - Not Found diff --git a/payment_adyen_vsf/controllers/main.py b/payment_adyen_vsf/controllers/main.py index 9d9cf79..d4d139d 100644 --- a/payment_adyen_vsf/controllers/main.py +++ b/payment_adyen_vsf/controllers/main.py @@ -1,20 +1,20 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging import pprint -import werkzeug +import werkzeug from werkzeug import urls -from odoo import http, _ -from odoo.http import request +from odoo import _, http from odoo.exceptions import ValidationError +from odoo.http import request + from odoo.addons.payment import utils as payment_utils +from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing from odoo.addons.payment_adyen import utils as adyen_utils from odoo.addons.payment_adyen.controllers.main import AdyenController -from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing _logger = logging.getLogger(__name__) @@ -23,127 +23,182 @@ class AdyenControllerInherit(AdyenController): _webhook_url = AdyenController()._webhook_url - @http.route('/payment/adyen/payments', type='json', auth='public') + @http.route("/payment/adyen/payments", type="json", auth="public") def adyen_payments( - self, provider_id, reference, converted_amount, currency_id, partner_id, payment_method, - access_token, browser_info=None + self, + provider_id, + reference, + converted_amount, + currency_id, + partner_id, + payment_method, + access_token, + browser_info=None, ): - """ Make a payment request and process the feedback data. + """Make a payment request and process the feedback data. - :param int provider_id: The provider handling the transaction, as a `payment.provider` id + :param int provider_id: The provider handling the transaction, as + `payment.provider` id :param str reference: The reference of the transaction - :param int converted_amount: The amount of the transaction in minor units of the currency + :param int converted_amount: The amount of the transaction in minor + units of the currency :param int currency_id: The currency of the transaction, as a `res.currency` id :param int partner_id: The partner making the transaction, as a `res.partner` id - :param dict payment_method: The details of the payment method used for the transaction + :param dict payment_method: The details of the payment method used for + the transaction :param str access_token: The access token used to verify the provided values :param dict browser_info: The browser info to pass to Adyen :return: The JSON-formatted content of the response :rtype: dict """ - # Check that the transaction details have not been altered. This allows preventing users + # Check that the transaction details have not been altered. This allows + # preventing users # from validating transactions by paying less than agreed upon. if not payment_utils.check_access_token( - access_token, reference, converted_amount, partner_id + access_token, reference, converted_amount, partner_id ): - raise ValidationError("Adyen: " + _("Received tampered payment request data.")) + raise ValidationError(_("Adyen: Received tampered payment request data.")) # Make the payment request to Adyen - provider_sudo = request.env['payment.provider'].sudo().browse(provider_id).exists() - tx_sudo = request.env['payment.transaction'].sudo().search([('reference', '=', reference)]) + provider_sudo = ( + request.env["payment.provider"].sudo().browse(provider_id).exists() + ) + tx_sudo = ( + request.env["payment.transaction"] + .sudo() + .search([("reference", "=", reference)]) + ) shopper_ip = payment_utils.get_customer_ip_address() if tx_sudo.created_on_vsf: - if request.httprequest.headers.environ.get('HTTP_REAL_IP', False) and \ - request.httprequest.headers.environ['HTTP_REAL_IP']: - shopper_ip = request.httprequest.headers.environ['HTTP_REAL_IP'] + if ( + request.httprequest.headers.environ.get("HTTP_REAL_IP", False) + and request.httprequest.headers.environ["HTTP_REAL_IP"] + ): + shopper_ip = request.httprequest.headers.environ["HTTP_REAL_IP"] data = { - 'merchantAccount': provider_sudo.adyen_merchant_account, - 'amount': { - 'value': converted_amount, - 'currency': request.env['res.currency'].browse(currency_id).name, # ISO 4217 + "merchantAccount": provider_sudo.adyen_merchant_account, + "amount": { + "value": converted_amount, + "currency": request.env["res.currency"] + .browse(currency_id) + .name, # ISO 4217 }, - 'reference': reference, - 'paymentMethod': payment_method, - 'shopperReference': provider_sudo._adyen_compute_shopper_reference(partner_id), - 'recurringProcessingModel': 'CardOnFile', # Most susceptible to trigger a 3DS check - 'shopperIP': shopper_ip, - 'shopperInteraction': 'Ecommerce', - 'shopperEmail': tx_sudo.partner_email, - 'shopperName': adyen_utils.format_partner_name(tx_sudo.partner_name), - 'telephoneNumber': tx_sudo.partner_phone, - 'storePaymentMethod': tx_sudo.tokenize, # True by default on Adyen side + "reference": reference, + "paymentMethod": payment_method, + "shopperReference": provider_sudo._adyen_compute_shopper_reference( + partner_id + ), + # Most susceptible to trigger a 3DS check + "recurringProcessingModel": "CardOnFile", + "shopperIP": shopper_ip, + "shopperInteraction": "Ecommerce", + "shopperEmail": tx_sudo.partner_email, + "shopperName": adyen_utils.format_partner_name(tx_sudo.partner_name), + "telephoneNumber": tx_sudo.partner_phone, + "storePaymentMethod": tx_sudo.tokenize, # True by default on Adyen side # 'additionalData': { # 'allow3DS2': True # }, - 'channel': 'web', # Required to support 3DS - 'origin': provider_sudo.get_base_url(), # Required to support 3DS - 'browserInfo': browser_info, # Required to support 3DS - 'returnUrl': urls.url_join( + "channel": "web", # Required to support 3DS + "origin": provider_sudo.get_base_url(), # Required to support 3DS + "browserInfo": browser_info, # Required to support 3DS + "returnUrl": urls.url_join( provider_sudo.get_base_url(), - # Include the reference in the return url to be able to match it after redirection. - # The key 'merchantReference' is chosen on purpose to be the same as that returned + # Include the reference in the return url to be able to match it + # after redirection. + # The key 'merchantReference' is chosen on purpose to be the + # same as that returned # by the /payments endpoint of Adyen. - f'/payment/adyen/return?merchantReference={reference}' + f"/payment/adyen/return?merchantReference={reference}", ), **adyen_utils.include_partner_addresses(tx_sudo), } - # Force the capture delay on Adyen side if the provider is not configured for capturing + # Force the capture delay on Adyen side if the provider is not configured + # for capturing # payments manually. This is necessary because it's not possible to distinguish - # 'AUTHORISATION' events sent by Adyen with the merchant account's capture delay set to - # 'manual' from events with the capture delay set to 'immediate' or a number of hours. If - # the merchant account is configured to capture payments with a delay but the provider is - # not, we force the immediate capture to avoid considering authorized transactions as + # 'AUTHORISATION' events sent by Adyen with the merchant account's capture + # delay set to + # 'manual' from events with the capture delay set to 'immediate' or a number + # of hours. If + # the merchant account is configured to capture payments with a delay but + # the provider is + # not, we force the immediate capture to avoid considering authorized + # transactions as # captured on Odoo. if not provider_sudo.capture_manually: data.update(captureDelayHours=0) # Make the payment request to Adyen response_content = provider_sudo._adyen_make_request( - url_field_name='adyen_checkout_api_url', - endpoint='/payments', + url_field_name="adyen_checkout_api_url", + endpoint="/payments", payload=data, - method='POST' + method="POST", ) # Handle the payment request response _logger.info( "payment request response for transaction with reference %s:\n%s", - reference, pprint.pformat(response_content) + reference, + pprint.pformat(response_content), ) tx_sudo._handle_notification_data( - 'adyen', dict(response_content, merchantReference=reference), # Match the transaction + "adyen", + dict( + response_content, merchantReference=reference + ), # Match the transaction ) return response_content - @http.route('/payment/adyen/return', type='http', auth='public', csrf=False, save_session=False) + @http.route( + "/payment/adyen/return", + type="http", + auth="public", + csrf=False, + save_session=False, + ) def adyen_return_from_3ds_auth(self, **data): - """ Process the authentication data sent by Adyen after redirection from the 3DS1 page. - - The route is flagged with `save_session=False` to prevent Odoo from assigning a new session - to the user if they are redirected to this route with a POST request. Indeed, as the session - cookie is created without a `SameSite` attribute, some browsers that don't implement the - recommended default `SameSite=Lax` behavior will not include the cookie in the redirection - request from the payment provider to Odoo. As the redirection to the '/payment/status' page - will satisfy any specification of the `SameSite` attribute, the session of the user will be - retrieved and with it the transaction which will be immediately post-processed. - - :param dict data: The authentication result data. May include custom params sent to Adyen in - the request to allow matching the transaction when redirected here. + """Process the authentication data sent by Adyen. + + Its done after redirection from the 3DS1 page. + + The route is flagged with `save_session=False` to prevent Odoo + from assigning a new session + to the user if they are redirected to this route with a POST + request. Indeed, as the session + cookie is created without a `SameSite` attribute, some browsers + that don't implement the + recommended default `SameSite=Lax` behavior will not include the + cookie in the redirection + request from the payment provider to Odoo. As the redirection to + the '/payment/status' page + will satisfy any specification of the `SameSite` attribute, the + session of the user will be + retrieved and with it the transaction which will be immediately + post-processed. + + :param dict data: The authentication result data. May include custom + params sent to Adyen in the request to allow matching the + transaction when redirected here. """ - payment_transaction = data.get('merchantReference') and request.env['payment.transaction'].sudo().search( - [('reference', 'in', [data.get('merchantReference')])], limit=1 - ) + payment_transaction = data.get("merchantReference") and request.env[ + "payment.transaction" + ].sudo().search([("reference", "in", [data.get("merchantReference")])], limit=1) # Check the Order and respective website related with the transaction # Check the payment_return url for the success and error pages # Pass the transaction_id on the session sale_order_ids = payment_transaction.sale_order_ids.ids - sale_order = request.env['sale.order'].sudo().search([ - ('id', 'in', sale_order_ids), ('website_id', '!=', False) - ], limit=1) + sale_order = ( + request.env["sale.order"] + .sudo() + .search( + [("id", "in", sale_order_ids), ("website_id", "!=", False)], limit=1 + ) + ) # Get Website website = sale_order.website_id @@ -154,34 +209,46 @@ def adyen_return_from_3ds_auth(self, **data): request.session["__payment_monitored_tx_ids__"] = [payment_transaction.id] # Retrieve the transaction based on the reference included in the return url - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( - 'adyen', data + tx_sudo = ( + request.env["payment.transaction"] + .sudo() + ._get_tx_from_notification_data("adyen", data) ) - # Overwrite the operation to force the flow to 'redirect'. This is necessary because even - # thought Adyen is implemented as a direct payment provider, it will redirect the user out - # of Odoo in some cases. For instance, when a 3DS1 authentication is required, or for + # Overwrite the operation to force the flow to 'redirect'. This is + # necessary because even + # thought Adyen is implemented as a direct payment provider, it will + # redirect the user out + # of Odoo in some cases. For instance, when a 3DS1 authentication is + # required, or for # special payment methods that are not handled by the drop-in (e.g. Sofort). - tx_sudo.operation = 'online_redirect' + tx_sudo.operation = "online_redirect" - # Query and process the result of the additional actions that have been performed + # Query and process the result of the additional actions that have been + # performed _logger.info( - "handling redirection from Adyen for transaction with reference %s with data:\n%s", - tx_sudo.reference, pprint.pformat(data) + "handling redirection from Adyen for transaction with reference" + + " %s with data:\n%s", + tx_sudo.reference, + pprint.pformat(data), ) result = self.adyen_payment_details( tx_sudo.provider_id.id, - data['merchantReference'], + data["merchantReference"], { - 'details': { - 'redirectResult': data['redirectResult'], + "details": { + "redirectResult": data["redirectResult"], }, }, ) if payment_transaction.created_on_vsf: # For Redirect 3DS2 and MobilePay (Success flow) - if result and result.get('resultCode') and result['resultCode'] == 'Authorised': + if ( + result + and result.get("resultCode") + and result["resultCode"] == "Authorised" + ): # Confirm sale order PaymentPostProcessing().poll_status() @@ -189,81 +256,115 @@ def adyen_return_from_3ds_auth(self, **data): return werkzeug.utils.redirect(vsf_payment_success_return_url) # For Redirect 3DS2 and MobilePay (Cancel/Error flow) - elif result and result.get('resultCode') and result['resultCode'] in ['Refused', 'Cancelled']: + elif ( + result + and result.get("resultCode") + and result["resultCode"] in ["Refused", "Cancelled"] + ): return werkzeug.utils.redirect(vsf_payment_error_return_url) else: # Redirect the user to the status page - return request.redirect('/payment/status') + return request.redirect("/payment/status") - @http.route(_webhook_url, type='json', auth='public') + @http.route(_webhook_url, type="json", auth="public") def adyen_webhook(self): - """ Process the data sent by Adyen to the webhook based on the event code. + """Process the data sent by Adyen to the webhook based on the event code. - See https://docs.adyen.com/development-resources/webhooks/understand-notifications for the - exhaustive list of event codes. + See + https://docs.adyen.com/development-resources/webhooks + /understand-notifications + for the exhaustive list of event codes. :return: The '[accepted]' string to acknowledge the notification :rtype: str """ data = request.dispatcher.jsonrequest - for notification_item in data['notificationItems']: - notification_data = notification_item['NotificationRequestItem'] + for notification_item in data["notificationItems"]: + notification_data = notification_item["NotificationRequestItem"] _logger.info( - "notification received from Adyen with data:\n%s", pprint.pformat(notification_data) + "notification received from Adyen with data:\n%s", + pprint.pformat(notification_data), ) - PaymentTransaction = request.env['payment.transaction'] + PaymentTransaction = request.env["payment.transaction"] try: - payment_transaction = notification_data.get('merchantReference') and PaymentTransaction.sudo().search( - [('reference', 'in', [notification_data.get('merchantReference')])], limit=1 + payment_transaction = notification_data.get( + "merchantReference" + ) and PaymentTransaction.sudo().search( + [("reference", "in", [notification_data.get("merchantReference")])], + limit=1, ) # Check the integrity of the notification - tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( - 'adyen', notification_data + tx_sudo = ( + request.env["payment.transaction"] + .sudo() + ._get_tx_from_notification_data("adyen", notification_data) ) self._verify_notification_signature(notification_data, tx_sudo) - # Check whether the event of the notification succeeded and reshape the notification + # Check whether the event of the notification succeeded and + # reshape the notification # data for parsing - success = notification_data['success'] == 'true' - event_code = notification_data['eventCode'] - if event_code == 'AUTHORISATION' and success: - notification_data['resultCode'] = 'Authorised' - elif event_code == 'CANCELLATION': - notification_data['resultCode'] = 'Cancelled' if success else 'Error' - elif event_code in ['REFUND', 'CAPTURE']: - notification_data['resultCode'] = 'Authorised' if success else 'Error' + success = notification_data["success"] == "true" + event_code = notification_data["eventCode"] + if event_code == "AUTHORISATION" and success: + notification_data["resultCode"] = "Authorised" + elif event_code == "CANCELLATION": + notification_data["resultCode"] = ( + "Cancelled" if success else "Error" + ) + elif event_code in ["REFUND", "CAPTURE"]: + notification_data["resultCode"] = ( + "Authorised" if success else "Error" + ) else: continue # Don't handle unsupported event codes and failed events - # Handle the notification data as if they were feedback of a S2S payment request - tx_sudo._handle_notification_data('adyen', notification_data) + # Handle the notification data as if they were feedback of a + # S2S payment request + tx_sudo._handle_notification_data("adyen", notification_data) # Case the transaction was created on vsf (Success flow) - if event_code == 'AUTHORISATION' and success and payment_transaction.created_on_vsf: - # Check the Order and respective website related with the transaction - # Check the payment_return url for the success and error pages + if ( + event_code == "AUTHORISATION" + and success + and payment_transaction.created_on_vsf + ): + # Check the Order and respective website related with the + # transaction Check the payment_return url for the success + # and error pages sale_order_ids = payment_transaction.sale_order_ids.ids - sale_order = request.env['sale.order'].sudo().search([ - ('id', 'in', sale_order_ids), ('website_id', '!=', False) - ], limit=1) + sale_order = ( + request.env["sale.order"] + .sudo() + .search( + [("id", "in", sale_order_ids), ("website_id", "!=", False)], + limit=1, + ) + ) # Get Website website = sale_order.website_id # Redirect to VSF - vsf_payment_success_return_url = website.vsf_payment_success_return_url + vsf_payment_success_return_url = ( + website.vsf_payment_success_return_url + ) - request.session["__payment_monitored_tx_ids__"] = [payment_transaction.id] + request.session["__payment_monitored_tx_ids__"] = [ + payment_transaction.id + ] # Confirm sale order PaymentPostProcessing().poll_status() return werkzeug.utils.redirect(vsf_payment_success_return_url) + # Acknowledge the notification to avoid getting spammed + except ValidationError: + _logger.exception( + "unable to handle the notification data; skipping to acknowledge" + ) - except ValidationError: # Acknowledge the notification to avoid getting spammed - _logger.exception("unable to handle the notification data; skipping to acknowledge") - - return '[accepted]' # Acknowledge the notification + return "[accepted]" # Acknowledge the notification diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a6cc9c1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +filterwarnings = + #action:message:category:module:line + # Odoo has some old code and Python is complaining. + ignore:invalid escape sequence.*:DeprecationWarning + # mrbob uses outdated code. + ignore:The SafeConfigParser class has been renamed.*:DeprecationWarning diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ba0a5cc --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[pydocstyle] +ignore = D100,D101,D102,D103,D104,D106,D107,D203,D213,D406,D407 +[flake8] +ignore = E203,W503 +max-line-length = 88 +per-file-ignores= + __init__.py:F401 +[bandit] +# B101: Make no sense that using assert is security issue. +# B410: https://github.com/tiran/defusedxml/issues/31 +# B404, B603, B607: to use subprocess +skips = B101,B404,B410,B603,B607 diff --git a/vuestorefront/README.rst b/vuestorefront/README.rst new file mode 100644 index 0000000..7ac3aec --- /dev/null +++ b/vuestorefront/README.rst @@ -0,0 +1,171 @@ +============== +Vue Storefront +============== + +This is a forked module previously called ``graphql_vuestorefront``. It +is backwards incompatible module, so it should not be installed +alongside ``graphql_vuestorefront``. Custom changes different than in +forked module: + +* ``get_product_list`` in ``schemas.product`` now uses offset/limit in standard + search instead of searching for all possible products and then slicing ( + which has terrible performance). +* ``website_slug``: (renamed to ``slug``) got rid of translate awareness ( + to simplify it), and not adding + slashes in slug, because slug is not supposed to have that. (on category, removed + validation as it was forcing to use slash. Added unique constraint, because + category slug can be entered manually). +* Split odoo model files into more appropriate, to make it more readable and + maintainable. +* Removed ``public_categ_slug_ids`` field as it was confusing and redundant + field that was very slow to compute for large amount of products. +* implemented ``products`` ``category_slug`` filter. +* moved Product schema domain method to ``product.template`` to be extendable. + +Addresses +--------- + +* Moved address domain methods on ``res.partner`` to be extendable. +* Moved preparation of partner (address) creation/update on ``res.partner to + be extendable. + +Sale Order +---------- + +* Added UpdateOrder mutation to be able to update some data on sale order. + +Users +----- + +* Can pass extra data when signing up (only ``vat`` for now). + +Login +===== + +To authenticate, use the default /web/session/authenticate endpoint. +Example using axios: + +.. code-block:: + + axios.post('/web/session/authenticate', { + "jsonrpc": "2.0", + "method": "call", + "params": { + "db": , + "login": , + "password": + }}, { + "withCredentials": true + }) + +Logout +====== + +.. code-block:: + + axios.post('/web/session/destroy', { + "jsonrpc": "2.0", + "method": "call" + }, { + "withCredentials": true + }) + +Add to Cart +=========== + +.. code-block:: + + axios.post('/shop/cart/update_json', { + "jsonrpc": "2.0", + "method": "call", + "params": { + "product_id": , + "add_qty": + }}, { + "withCredentials": true + }) + +Add to wishlist +=============== + +.. code-block:: + + axios.post('/shop/wishlist/add', { + "jsonrpc": "2.0", + "method": "call", + "params": { + "product_id": , + }}, { + "withCredentials": true + }) + +Remove from wishlist +==================== + +.. code-block:: + + axios.post('/shop/wishlist/remove/', { + "jsonrpc": "2.0", + "method": "call" + }, { + "withCredentials": true + }) + +Get the rate for a shipping method +================================== + +.. code-block:: + + axios.post('/shop/carrier_rate_shipment', { + "jsonrpc": "2.0", + "method": "call" + "params": { + "carrier_id": , + }}, { + "withCredentials": true + }) + +Get all product template attributes for product template page +============================================================= + +.. code-block:: + + axios.post('/shop/get_combinations/', { + "jsonrpc": "2.0", + "method": "call" + }, { + "withCredentials": true + }) + +Get product id and price after selecting the combination on the product template page +===================================================================================== + +.. code-block:: + + axios.post('/shop/get_combination_info/', { + "jsonrpc": "2.0", + "method": "call" + "params": { + "combination_ids": [1, 2], + add_qty=1 + }}, { + "withCredentials": true + }) + +Get products for shop with search, category, sort, count, pagination and attributes filtering +============================================================================================= + +.. code-block:: + + axios.post('/shop/products', { + "jsonrpc": "2.0", + "method": "call" + "params": { + "search": "", + "category_id": 1, + "offset": 0, + "ppg": 20, + "attrib_list": [] + }}, { + "withCredentials": true + }) diff --git a/graphql_vuestorefront/__init__.py b/vuestorefront/__init__.py similarity index 58% rename from graphql_vuestorefront/__init__.py rename to vuestorefront/__init__.py index b69bb5c..fd82b3d 100644 --- a/graphql_vuestorefront/__init__.py +++ b/vuestorefront/__init__.py @@ -1,10 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from . import controllers from . import models -from .hooks import ( - pre_init_hook_login_check, - post_init_hook_login_convert -) +from .hooks import pre_init_hook_login_check, post_init_hook_login_convert diff --git a/vuestorefront/__manifest__.py b/vuestorefront/__manifest__.py new file mode 100644 index 0000000..66f1c0e --- /dev/null +++ b/vuestorefront/__manifest__.py @@ -0,0 +1,50 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Vue Storefront Api", + "version": "16.0.1.0.0", + "summary": "Vue Storefront API", + "category": "Website", + "license": "LGPL-3", + "author": "OdooGap", + "website": "https://www.odoogap.com/", + "depends": [ + "graphql_base", + "website_sale_wishlist", + "website_sale_delivery", + "website_mass_mailing", + "website_sale_loyalty", + "auth_signup", + "contacts", + "crm", + "theme_default", + "payment_adyen_vsf", + ], + "data": [ + "security/ir.model.access.csv", + "data/website_data.xml", + "data/mail_template.xml", + "data/ir_config_parameter_data.xml", + "data/ir_cron_data.xml", + "views/product_views.xml", + "views/website_views.xml", + "views/res_config_settings_views.xml", + ], + "demo": [ + "data/demo_product_attribute.xml", + "data/demo_product_public_category.xml", + "data/demo_products_women_clothing.xml", + "data/demo_products_women_shoes.xml", + "data/demo_products_women_bags.xml", + "data/demo_products_men_clothing_1.xml", + "data/demo_products_men_clothing_2.xml", + "data/demo_products_men_clothing_3.xml", + "data/demo_products_men_clothing_4.xml", + "data/demo_products_men_shoes.xml", + ], + "installable": True, + "auto_install": False, + "pre_init_hook": "pre_init_hook_login_check", + "post_init_hook": "post_init_hook_login_convert", +} diff --git a/graphql_vuestorefront/controllers/__init__.py b/vuestorefront/controllers/__init__.py similarity index 84% rename from graphql_vuestorefront/controllers/__init__.py rename to vuestorefront/controllers/__init__.py index fbf9bf7..fee1a97 100644 --- a/graphql_vuestorefront/controllers/__init__.py +++ b/vuestorefront/controllers/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/vuestorefront/controllers/main.py b/vuestorefront/controllers/main.py new file mode 100644 index 0000000..865203d --- /dev/null +++ b/vuestorefront/controllers/main.py @@ -0,0 +1,262 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json +import logging +import os +import pprint +from urllib.parse import urlparse + +from odoo import http +from odoo.http import Response, request + +from odoo.addons.graphql_base import GraphQLControllerMixin +from odoo.addons.web.controllers.binary import Binary + +from ..schema import schema + +_logger = logging.getLogger(__name__) + + +class VSFBinary(Binary): + @http.route( + [ + "/web/image", + "/web/image/", + "/web/image//", + "/web/image//x", + "/web/image//x/", + "/web/image///", + "/web/image////", + ( + "/web/image////" + + "x" + ), + ( + "/web/image///" + + "/x/" + ), + "/web/image/", + "/web/image//", + "/web/image//x", + "/web/image//x/", + "/web/image/-", + "/web/image/-/", + "/web/image/-/x", + ( + "/web/image/-/" + + "x/" + ), + ], + type="http", + auth="public", + ) + def content_image( + self, + xmlid=None, + model="ir.attachment", + id=None, + field="datas", + filename_field="name", + unique=None, + filename=None, + mimetype=None, + download=None, + width=0, + height=0, + crop=False, + access_token=None, + **kwargs, + ): + """Validate width and height.""" + try: + ICP = request.env["ir.config_parameter"].sudo() + vsf_image_resize_limit = int(ICP.get_param("vsf_image_resize_limit", 1920)) + + if width > vsf_image_resize_limit or height > vsf_image_resize_limit: + return request.not_found() + except Exception: + return request.not_found() + + return super().content_image( + xmlid=xmlid, + model=model, + id=id, + field=field, + filename_field=filename_field, + unique=unique, + filename=filename, + mimetype=mimetype, + download=download, + width=width, + height=height, + crop=crop, + access_token=access_token, + **kwargs, + ) + + +class GraphQLController(http.Controller, GraphQLControllerMixin): + def _process_request(self, schema, data): + # Set the vsf_debug_mode value that exist in the settings + ICP = http.request.env["ir.config_parameter"].sudo() + vsf_debug_mode = ICP.get_param("vsf_debug_mode", False) + if vsf_debug_mode: + try: + request = http.request.httprequest + _logger.info( + "# ------------------------------- GRAPHQL: DEBUG MODE" + + " -------------------------------- #" + ) + _logger.info("") + _logger.info( + "# ------------------------------------------------------- #" + ) + _logger.info( + "# HEADERS #" + ) + _logger.info( + "# ------------------------------------------------------- #" + ) + _logger.info("\n%s", pprint.pformat(request.headers.environ)) + _logger.info("") + _logger.info( + "# ------------------------------------------------------- #" + ) + _logger.info( + "# QUERY / MUTATION #" + ) + _logger.info( + "# ------------------------------------------------------- #" + ) + _logger.info("\n%s", data.get("query", None)) + _logger.info("") + _logger.info( + "# ------------------------------------------------------- #" + ) + _logger.info( + "# ARGUMENTS #" + ) + _logger.info( + "# ------------------------------------------------------- #" + ) + _logger.info("\n%s", request.args.get("variables", None)) + _logger.info("") + _logger.info("# ----------------------------------------------------#") + except Exception as e: + _logger.error( + "Something went wrong processing request: %s", e, exc_info=True + ) + return super()._process_request(schema, data) + + def _set_website_context(self): + """Set website context based on http_request_host header.""" + try: + request_host = request.httprequest.headers.environ["HTTP_RESQUEST_HOST"] + website = request.env["website"].search( + [("domain", "ilike", request_host)], limit=1 + ) + if website: + context = dict(request.context) + context.update( + { + "website_id": website.id, + "lang": website.default_lang_id.code, + } + ) + request.context = context + + request_uid = http.request.env.uid + website_uid = website.sudo().user_id.id + + if request_uid != website_uid and request.env[ + "res.users" + ].sudo().browse(request_uid).has_group("base.group_public"): + request.uid = website_uid + except Exception as e: + _logger.error( + "Something went wrong setting website context: %s", e, exc_info=True + ) + + # The GraphiQL route, providing an IDE for developers + @http.route("/graphiql/vsf", auth="user") + def graphiql(self, **kwargs): + self._set_website_context() + return self._handle_graphiql_request(schema.graphql_schema) + + # The graphql route, for applications. + # Note csrf=False: you may want to apply extra security + # (such as origin restrictions) to this route. + @http.route("/graphql/vsf", auth="public", csrf=False) + def graphql(self, **kwargs): + self._set_website_context() + return self._handle_graphql_request(schema.graphql_schema) + + @http.route("/vsf/categories", type="http", auth="public", csrf=False) + def vsf_categories(self): + self._set_website_context() + website = request.env["website"].get_current_website() + + categories = [] + + if website.default_lang_id: + lang_code = website.default_lang_id.code + domain = [("slug", "!=", False)] + + for category in ( + request.env["product.public.category"].sudo().search(domain) + ): + category = category.with_context(lang=lang_code) + categories.append(category.slug) + + return Response( + json.dumps(categories), + headers={"Content-Type": "application/json"}, + ) + + @http.route("/vsf/products", type="http", auth="public", csrf=False) + def vsf_products(self): + self._set_website_context() + website = request.env["website"].get_current_website() + + products = [] + + if website.default_lang_id: + lang_code = website.default_lang_id.code + domain = [("website_published", "=", True), ("slug", "!=", False)] + + for product in request.env["product.template"].sudo().search(domain): + product = product.with_context(lang=lang_code) + + url_parsed = urlparse(product.slug) + name = os.path.basename(url_parsed.path) + path = product.slug.replace(name, "") + + products.append( + { + "name": name, + "path": f"{path}:slug", + } + ) + + return Response( + json.dumps(products), + headers={"Content-Type": "application/json"}, + ) + + @http.route("/vsf/redirects", type="http", auth="public", csrf=False) + def vsf_redirects(self): + redirects = [] + + for redirect in request.env["website.rewrite"].sudo().search([]): + redirects.append( + { + "from": redirect.url_from, + "to": redirect.url_to, + } + ) + + return Response( + json.dumps(redirects), + headers={"Content-Type": "application/json"}, + ) diff --git a/graphql_vuestorefront/data/demo_product_attribute.xml b/vuestorefront/data/demo_product_attribute.xml similarity index 100% rename from graphql_vuestorefront/data/demo_product_attribute.xml rename to vuestorefront/data/demo_product_attribute.xml diff --git a/graphql_vuestorefront/data/demo_product_public_category.xml b/vuestorefront/data/demo_product_public_category.xml similarity index 93% rename from graphql_vuestorefront/data/demo_product_public_category.xml rename to vuestorefront/data/demo_product_public_category.xml index 6e7d477..9cbdb4c 100644 --- a/graphql_vuestorefront/data/demo_product_public_category.xml +++ b/vuestorefront/data/demo_product_public_category.xml @@ -68,7 +68,10 @@ Shirts 33 - + T-shirts 33 @@ -93,7 +96,10 @@ Dresses 38 - + Swimwear 39 @@ -115,7 +121,10 @@ Boots 43 - + Ankle boots 44 @@ -130,7 +139,10 @@ Ballerinas 46 - + Lace-up shoes 47 @@ -145,7 +157,10 @@ Sandals 49 - + Winterboots 50 @@ -162,7 +177,10 @@ Clutches 53 - + Shoulder Bags 54 @@ -182,7 +200,10 @@ Wallets 57 - + Bucketbag/packbag 58 @@ -280,7 +301,10 @@ Boots 75 - + Lace-up shoes 76 @@ -327,7 +351,10 @@ Wallets 85 - + Bucketbag/packbag 86 diff --git a/vuestorefront/data/demo_products_men_clothing_1.xml b/vuestorefront/data/demo_products_men_clothing_1.xml new file mode 100644 index 0000000..d101f9f --- /dev/null +++ b/vuestorefront/data/demo_products_men_clothing_1.xml @@ -0,0 +1,1365 @@ + + + + + + + Daniele Alessandrini – Vest + + 165.00 + + 165.00 + product + + + + This is the product: Daniele Alessandrini - Vest + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DA01-01 + 165.00 + + + + DA01-02 + 165.00 + + + + DA01-03 + 165.00 + + + + DA01-04 + 165.00 + + + + DA01-05 + 165.00 + 170.00 + + + + DA01-06 + 165.00 + 170.00 + + + + + + + Leather jacket Bully dark blue + + 523.75 + + 523.75 + product + + + + This is the product: Leather jacket Bully dark blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LJBD01-01 + 523.75 + + + + LJBD01-02 + 523.75 + + + + + + + Bomber Daniele Alessandrini blue + + 356.25 + + 356.25 + product + + + + This is the product: Bomber Daniele Alessandrini blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BDA01-01 + 523.75 + + + + BDA01-02 + 523.75 + + + + + + + Save the Duck – Casual Jacket + + 161.25 + + 161.25 + product + + + + This is the product: Save the Duck – Casual Jacket + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CJ01-01 + 161.25 + + + + CJ01-02 + 161.25 + + + + CJ01-03 + 161.25 + + + + CJ01-04 + 161.25 + + + + CJ01-05 + 161.25 + + + + CJ01-06 + 161.25 + + + + + + + Vest ”Naples” Moncler red + + 662.50 + + 662.50 + product + + + + This is the product: Vest ”Naples” Moncler red + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VNMR01-01 + 662.50 + + + + VNMR01-02 + 662.50 + + + + VNMR01-03 + 662.50 + + + + VNMR01-04 + 662.50 + + + + + + + Moncler – Down Jacket “Jacob” + + 1218.75 + + 1218.75 + product + + + + This is the product: Moncler – Down Jacket “Jacob” + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MDJ01-01 + 1218.75 + + + + MDJ01-02 + 1218.75 + + + + MDJ01-03 + 1218.75 + + + + + + + + Casual jacket Aspesi blue + + 430.00 + + 430.00 + product + + + + This is the product: Casual jacket Aspesi blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CJB01-01 + 430.00 + + + + CJB01-02 + 430.00 + + + + + + + Casual jacket “Mahakali“ Peuterey blue + + 423.75 + + 423.75 + product + + + + This is the product: Casual jacket “Mahakali“ Peuterey blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mahakali-01 + 423.75 + + + + Mahakali-02 + 423.75 + + + + + + + Casual jacket Invicta blue + + 173.75 + + 173.75 + product + + + + This is the product: Casual jacket Invicta blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Invicta-01 + 423.75 + + + + Invicta-02 + 423.75 + + + + + + + + Casual jacket ”Lyon” Moncler black + + 656.25 + + 656.25 + product + + + + This is the product: Casual jacket ”Lyon” Moncler black + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LYON-01 + 656.25 + + + + LYON-02 + 656.25 + + + + + diff --git a/vuestorefront/data/demo_products_men_clothing_2.xml b/vuestorefront/data/demo_products_men_clothing_2.xml new file mode 100644 index 0000000..3340bb1 --- /dev/null +++ b/vuestorefront/data/demo_products_men_clothing_2.xml @@ -0,0 +1,1212 @@ + + + + + + + Casual Jacket Save the Duck dark blue + + 198.75 + + 198.75 + product + + + + This is the product: Casual Jacket Save the Duck dark blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CJSDDB-01 + 198.75 + + + + + CJSDDB-02 + 198.75 + + + + + + + Leather jacket D.r.o.w.s black + + 872.50 + + 872.50 + product + + + + This is the product: Leather jacket D.r.o.w.s black + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LJB3-01 + 872.50 + + + + LJB3-02 + 872.50 + + + + + + + + Moncler – Down Jacket “Ryan” + + 1162.50 + + 1162.50 + product + + + + This is the product: Moncler – Down Jacket “Ryan” + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RYAN-01 + 1162.50 + + + + RYAN-02 + 1162.50 + + + + + + + Harris Wharf – Coat + + 598.75 + + 598.75 + product + + + + This is the product: Harris Wharf – Coat + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HARRISWARF-01 + 598.75 + + + + HARRISWARF-02 + 598.75 + + + + + + + Casual jacket Michael Kors beige + + 373.75 + + 373.75 + product + + + + This is the product: Casual jacket Michael Kors beige + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MKB02-01 + 598.75 + + + + + MKB02-02 + 598.75 + + + + + + + + Down jacket “Kathmandu“ Peuterey grey + + 411.25 + + 411.25 + product + + + + This is the product: Down jacket “Kathmandu“ Peuterey grey + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KATHMANDU-01 + 411.25 + + + + KATHMANDU-02 + 411.25 + + + + KATHMANDU-03 + 411.25 + + + + + + + + Coat Aspesi beige + + 536.25 + + 536.25 + product + + + + This is the product: Coat Aspesi beige + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAD02-01 + 536.25 + + + + CAD02-02 + 536.25 + + + + + + + + Casual jacket Stone Island grey + + 837.50 + + 837.50 + product + + + + This is the product: Casual jacket Stone Island grey + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CJSIG-01 + 837.50 + + + + CJSIG-02 + 837.50 + + + + + + + Jacket Doubleface “Sol Walk“ Luis Trenker blue + + 498.75 + + 498.75 + product + + + + This is the product: Jacket Doubleface “Sol Walk“ Luis Trenker blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SOLWALK-01 + 498.75 + + + + SOLWALK-02 + 498.75 + + + + + + + Bully – Leather Jacket + + 497.50 + + 497.50 + product + + + + This is the product: Bully – Leather Jacket + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BULLY02-01 + 497.50 + + + + + BULLY02-02 + 497.50 + + + + diff --git a/vuestorefront/data/demo_products_men_clothing_3.xml b/vuestorefront/data/demo_products_men_clothing_3.xml new file mode 100644 index 0000000..97262bc --- /dev/null +++ b/vuestorefront/data/demo_products_men_clothing_3.xml @@ -0,0 +1,600 @@ + + + + + + + Coat Aspesi blue + + 536.25 + + 536.25 + product + + + + This is the product: Coat Aspesi blue + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAB05-01 + 536.25 + + + + CAB05-02 + 536.25 + + + + + + + + Casual jacket Stone Island black + + 498.75 + + 498.75 + product + + + + This is the product: Casual jacket Stone Island black + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CJSIB06-01 + 498.75 + + + + + CJSIB06-02 + 498.75 + + + + + + + Vest Bully black + + 486.25 + + 486.25 + product + + + + This is the product: Vest Bully black + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VBB03-01 + 486.25 + + + + VBB03-02 + 486.25 + + + + + + + Leather jacket Daniele Alessandrini beige + + 736.25 + + 736.25 + product + + + + This is the product: Leather jacket Daniele Alessandrini beige + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LJDAB02-01 + 736.25 + + + + LJDAB02-02 + 736.25 + + + + + + + Jacket Doubleface “Sol Walk“ Luis Trenker red + + 498.75 + + 498.75 + product + + + + This is the product: Jacket Doubleface “Sol Walk“ Luis Trenker red + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JDSWLTR-01 + 498.75 + + + + JDSWLTR-02 + 498.75 + + + + + diff --git a/vuestorefront/data/demo_products_men_clothing_4.xml b/vuestorefront/data/demo_products_men_clothing_4.xml new file mode 100644 index 0000000..09c021a --- /dev/null +++ b/vuestorefront/data/demo_products_men_clothing_4.xml @@ -0,0 +1,1344 @@ + + + + + + + Cardigan Kangra green + + 200.00 + + 200.00 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 200.00 + + + 578902-00 + 200.00 + + + + + Vest Tagliatore blue + + 206.25 + + 206.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 206.25 + + + 578902-00 + 206.25 + + + + + + Shirt Barba blue + + 248.75 + + 248.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 248.75 + + + 578902-00 + 248.75 + + + + + Shirt Himons multi + + 148.75 + + 148.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 148.75 + + + 578902-00 + 148.75 + + + + + + Chino Paolo Pecora grey + + 281.25 + + 281.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 281.25 + + + 578902-00 + 281.25 + + + + + Daniele Alessandrini – Casual hosen + + 218.75 + + 218.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 218.75 + + + 578902-00 + 218.75 + + + + + + jeans Siviglia dark blue + + 243.75 + + 243.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 243.75 + + + 578902-00 + 243.75 + + + + + Jeans Closed grey + + 211.25 + + 211.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 211.25 + + + 578902-00 + 211.25 + + + + + + Blazer Tagliatore brown + + 573.75 + + 573.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 573.75 + + + 578902-00 + 573.75 + + + + + Blazer Circolo 1901 blue + + 411.25 + + 411.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 411.25 + + + 578902-00 + 411.25 + + + + + + Suit Mauro Grifoni grey + + 668.75 + + 668.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 668.75 + + + 578902-00 + 668.75 + + + + + Suit Tagliatore dark blue + + 862.50 + + 862.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 862.50 + + + 578902-00 + 862.50 + + + + + + Shirt The Sartorialist light blue + + 186.25 + + 186.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 186.25 + + + 578902-00 + 186.25 + + + + + Shirt Daniele Alessandrini grey + + 198.75 + + 198.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 198.75 + + + 578902-00 + 198.75 + + + diff --git a/vuestorefront/data/demo_products_men_shoes.xml b/vuestorefront/data/demo_products_men_shoes.xml new file mode 100644 index 0000000..ab9e484 --- /dev/null +++ b/vuestorefront/data/demo_products_men_shoes.xml @@ -0,0 +1,580 @@ + + + + + + + Sneaker – Lotto “Tokyo“ + + 137.50 + + 137.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 137.50 + + + 578902-00 + 137.50 + + + + + Sneakers “Spot“ Springa multi + + 186.25 + + 186.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 186.25 + + + 578902-00 + 186.25 + + + + + + Lace up shoes Tods dark blue + + 462.50 + + 462.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 462.50 + + + 578902-00 + 462.50 + + + + + Lace up shoes Tods blue + + 362.50 + + 362.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 362.50 + + + 578902-00 + 362.50 + + + + + + Mokassins “Daime“ Doucals brown + + 343.75 + + 343.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 343.75 + + + 578902-00 + 343.75 + + + + + Flip Flops “Top Mix“ Havaianas dark blue + + 27.50 + + 27.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 27.50 + + + 578902-00 + 27.50 + + + diff --git a/vuestorefront/data/demo_products_women_bags.xml b/vuestorefront/data/demo_products_women_bags.xml new file mode 100644 index 0000000..db94d7a --- /dev/null +++ b/vuestorefront/data/demo_products_women_bags.xml @@ -0,0 +1,1193 @@ + + + + + + + Michael Kors – Clutch “Daria” + + 281.25 + + 281.25 + product + + + + This is the product: Michael Kors – Clutch “Daria” + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 281.25 + + + + + Clutch ”Carol” Liebeskind black + + 198.75 + + 198.75 + product + + + + This is the product: Clutch ”Carol” Liebeskind black + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 198.75 + + + + + + Clutch “Jet Set Travel” small Michael Kors + + 106.25 + + 106.25 + product + + + + This is the product: Clutch “Jet Set Travel” small Michael Kors + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CJSTMK-01 + 106.25 + + + + + CJSTMK-02 + 106.25 + + + + CJSTMK-03 + 106.25 + + + + CJSTMK-04 + 106.25 + + + + CJSTMK-05 + 106.25 + + + + CJSTMK-06 + 106.25 + + + + + CJSTMK-07 + 106.25 + + + + + CJSTMK-08 + 106.25 + + + + + + 31.25 + + + + + + Bag Moschino Love black-white + + 190.00 + + 190.00 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 190.00 + + + + + DKNY – Bag + + 372.50 + + 372.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 372.50 + + + + + + Moschino Love – Shopper + + 256.25 + + 256.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 256.25 + + + + + + Michael Kors – Shopper “Jet Set Travel” + + 343.75 + + 343.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 343.75 + + + + + + Bag “Jet Set Travel” Michael Kors + + 368.75 + + 368.75 + product + + + + This is the product: Bag “Jet Set Travel” Michael Kors + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BJSTMK-01 + 368.75 + + + + BJSTMK-01 + 368.75 + + + + + 26.00 + + + + + + Guess – Hand bag “Nikki“ + + 173.75 + + 173.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 173.75 + + + + + Guess – handtaschen “Carnivale“ + + 181.25 + + 181.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 181.25 + + + + + + Wallet “Pervinca“ Gabs white + + 102.50 + + 102.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 102.50 + + + + + Gabs – Wallet “Gmoney” + + 106.25 + + 106.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 106.25 + + + diff --git a/vuestorefront/data/demo_products_women_clothing.xml b/vuestorefront/data/demo_products_women_clothing.xml new file mode 100644 index 0000000..c5b5c6d --- /dev/null +++ b/vuestorefront/data/demo_products_women_clothing.xml @@ -0,0 +1,1726 @@ + + + + + + + Leather jacket Bully grey + + 372.50 + + 372.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 372.50 + + + 578902-00 + 372.50 + + + + + Leather jacket Bully brown + + 372.50 + + 372.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 372.50 + + + 578902-00 + 372.50 + + + + + + Blazer Michael Kors brown + + 281.25 + + 281.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 281.25 + + + 578902-00 + 281.25 + + + + + Blazer Pinko yellow + + 337.50 + + 337.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 337.50 + + + 578902-00 + 337.50 + + + + + + Pullover Moschino Cheap And Chic black + + 247.50 + + 247.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 247.50 + + + 578902-00 + 247.50 + + + + + Pullover Moschino Cheap And Chic black + + 247.50 + + 247.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 247.50 + + + 578902-00 + 247.50 + + + + + + Shirt Aspesi white test + + 200.00 + + 200.00 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 200.00 + + + 578902-00 + 200.00 + + + + + Shirt Himons multi + + 148.75 + + 148.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 148.75 + + + 578902-00 + 148.75 + + + + + + Shirt ”Paola” MU blue + + 231.25 + + 231.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 231.25 + + + 578902-00 + 231.25 + + + + + Shirt Himons light blue + + 123.75 + + 123.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 123.75 + + + 578902-00 + 123.75 + + + + + + Biker jeans Pinko white + + 247.50 + + 247.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 247.50 + + + 578902-00 + 247.50 + + + + + jeans Michael Kors black + + 193.75 + + 193.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 193.75 + + + 578902-00 + 193.75 + + + + + + Jogging Pants Moschino Cheap And Chic black + + 286.25 + + 286.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 286.25 + + + 578902-00 + 286.25 + + + + + Chino Pinko multi + + 287.50 + + 287.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 287.50 + + + 578902-00 + 287.50 + + + + + + Skirt Ki 6? Who are you? blue + + 185.00 + + 185.00 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 185.00 + + + 578902-00 + 185.00 + + + + + Skirt Pinko beige + + 185.00 + + 185.00 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 185.00 + + + 578902-00 + 185.00 + + + + + + Dress Ki 6? Who are you? black + + 206.25 + + 206.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 206.25 + + + 578902-00 + 206.25 + + + + + Dress Moschino Cheap And Chic multi + + 320.00 + + 320.00 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 320.00 + + + 578902-00 + 320.00 + + + diff --git a/vuestorefront/data/demo_products_women_shoes.xml b/vuestorefront/data/demo_products_women_shoes.xml new file mode 100644 index 0000000..00741ff --- /dev/null +++ b/vuestorefront/data/demo_products_women_shoes.xml @@ -0,0 +1,1344 @@ + + + + + + + Sneakers “Jazz“ Saucony grey-green + + 118.75 + + 118.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 118.75 + + + 578902-00 + 118.75 + + + + + Sneakers Philippe Model grey + + 327.50 + + 327.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 327.50 + + + 578902-00 + 327.50 + + + + + + Booclothing Lerews beige + + 186.25 + + 186.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 186.25 + + + 578902-00 + 186.25 + + + + + Booclothing Lerews black + + 186.25 + + 186.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 186.25 + + + 578902-00 + 186.25 + + + + + + Booties Lemare black + + 231.25 + + 231.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 231.25 + + + 578902-00 + 231.25 + + + + + Booties Lemare brown + + 248.75 + + 248.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 248.75 + + + 578902-00 + 248.75 + + + + + + Pumps “Okala“ Sam Edelman grey + + 186.25 + + 186.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 186.25 + + + 578902-00 + 186.25 + + + + + Pumps ”H228” Hogan blue + + 362.50 + + 362.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 362.50 + + + 578902-00 + 362.50 + + + + + + Ballerina Liebeskind black-beige + + 123.75 + + 123.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 123.75 + + + 578902-00 + 123.75 + + + + + Ballerina Liebeskind multi + + 98.75 + + 98.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 98.75 + + + 578902-00 + 98.75 + + + + + + Loafers Alberto Guardiani gold + + 343.75 + + 343.75 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 343.75 + + + 578902-00 + 343.75 + + + + + Slip-On Shoes Crime silver + + 161.25 + + 161.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 161.25 + + + 578902-00 + 161.25 + + + + + + Sandals ”H257” Hogan beige + + 337.50 + + 337.50 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 337.50 + + + 578902-00 + 337.50 + + + + + sandalen “Georgie“ Sam Edelman brown + + 186.25 + + 186.25 + product + + + + The Karissa V-Neck Tee features a semi-fitted shape that's flattering for every figure. You can hit the gym with confidence while it hugs curves and hides common "problem" areas. Find stunning women's cocktail dresses and party dresses. + 578902-00 + delivery + + + + + + + + + + + + + + + + + + + + + + + + + + + 578902-00 + 186.25 + + + 578902-00 + 186.25 + + + diff --git a/graphql_vuestorefront/data/ir_config_parameter_data.xml b/vuestorefront/data/ir_config_parameter_data.xml similarity index 100% rename from graphql_vuestorefront/data/ir_config_parameter_data.xml rename to vuestorefront/data/ir_config_parameter_data.xml diff --git a/graphql_vuestorefront/data/ir_cron_data.xml b/vuestorefront/data/ir_cron_data.xml similarity index 90% rename from graphql_vuestorefront/data/ir_cron_data.xml rename to vuestorefront/data/ir_cron_data.xml index 7f98c1c..6258919 100644 --- a/graphql_vuestorefront/data/ir_cron_data.xml +++ b/vuestorefront/data/ir_cron_data.xml @@ -8,7 +8,7 @@ Request VSF Cache Invalidation - + code model.request_vsf_cache_invalidation() 5 diff --git a/vuestorefront/data/mail_template.xml b/vuestorefront/data/mail_template.xml new file mode 100644 index 0000000..a1e54e6 --- /dev/null +++ b/vuestorefront/data/mail_template.xml @@ -0,0 +1,232 @@ + + + + + + + Website Reset Password + + Password reset + "{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}> + {{ object.email_formatted }} + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ Your Account +
+ + Marc Demo + +
+ +
+
+
+
+ + + + + + + +
+
+ Dear Marc Demo, +
+
+ A password reset was requested for the Odoo account linked to this email. + You may change your password by following this link which will remain valid during 24 hours:
+ + If you do not expect this, you can safely ignore this email. +
+
+ Thanks, + +
+ --
Mitchell Admin
+
+
+
+
+
+
+ + + + + + + +
+ YourCompany +
+ +1 650-123-4567 + + | + + info@yourcompany.com + + + + | + + http://www.example.com + + +
+
+
+ + + + +
+ Powered by + Odoo + +
+
+
+ {{ object.lang }} + +
+
+
diff --git a/graphql_vuestorefront/data/website_data.xml b/vuestorefront/data/website_data.xml similarity index 66% rename from graphql_vuestorefront/data/website_data.xml rename to vuestorefront/data/website_data.xml index 1b9ba28..dd3bab3 100644 --- a/graphql_vuestorefront/data/website_data.xml +++ b/vuestorefront/data/website_data.xml @@ -9,8 +9,12 @@ - http://localhost:3000/checkout/thank-you - http://localhost:3000/payment-fail + http://localhost:3000/checkout/thank-you + http://localhost:3000/payment-fail diff --git a/graphql_vuestorefront/hooks.py b/vuestorefront/hooks.py similarity index 57% rename from graphql_vuestorefront/hooks.py rename to vuestorefront/hooks.py index 1af8d04..54dc637 100644 --- a/graphql_vuestorefront/hooks.py +++ b/vuestorefront/hooks.py @@ -1,20 +1,20 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import api, SUPERUSER_ID, _ +from odoo import SUPERUSER_ID, _, api from odoo.exceptions import ValidationError def pre_init_hook_login_check(cr): - """ - This hook will see if exists any conflict between Portal logins, before the module is installed + """Check if there are any conflict between portal logins. + + Check is done before module is installed. """ env = api.Environment(cr, SUPERUSER_ID, {}) check_users = [] - users = env['res.users'].search([]) + users = env["res.users"].search([]) for user in users: - if user.login and user.has_group('base.group_portal'): + if user.login and user.has_group("base.group_portal"): login = user.login.lower() if login not in check_users: check_users.append(login) @@ -25,11 +25,9 @@ def pre_init_hook_login_check(cr): def post_init_hook_login_convert(cr, registry): - """ - After the module is installed, set Portal Logins to lowercase - """ + """Set Portal Logins to lowercase.""" env = api.Environment(cr, SUPERUSER_ID, {}) - users = env['res.users'].search([]) + users = env["res.users"].search([]) for user in users: - if user.login and user.has_group('base.group_portal'): - user.login = user.login.lower() \ No newline at end of file + if user.login and user.has_group("base.group_portal"): + user.login = user.login.lower() diff --git a/vuestorefront/models/__init__.py b/vuestorefront/models/__init__.py new file mode 100644 index 0000000..f5556c0 --- /dev/null +++ b/vuestorefront/models/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from . import ( + invalidate_cache, + ir_http, + website, + product_public_category, + product_template, + product_product, + res_config_settings, + res_users, + payment_transaction, + res_partner, + sale_order, +) diff --git a/vuestorefront/models/invalidate_cache.py b/vuestorefront/models/invalidate_cache.py new file mode 100644 index 0000000..6e47c9f --- /dev/null +++ b/vuestorefront/models/invalidate_cache.py @@ -0,0 +1,161 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +from datetime import datetime + +import requests + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class InvalidateCache(models.Model): + _name = "invalidate.cache" + _description = "VSF Invalidate Cache" + + res_model = fields.Char(required=True, index=True) + res_id = fields.Integer("Res ID", required=True) + + def init(self): + super().init() + self.env.cr.execute( + """ + CREATE INDEX IF NOT EXISTS invalidate_cache_find_idx + ON invalidate_cache(res_model, res_id); + """ + ) + + @api.model + def find_invalidate_cache(self, res_model, res_id): + cr = self.env.cr + query = """ + SELECT id + FROM invalidate_cache + WHERE res_model=%s AND res_id=%s + LIMIT 1; + """ + params = ( + res_model, + res_id, + ) + + cr.execute(query, params) + return cr.fetchone() + + @api.model + def create_invalidate_cache(self, res_model, res_ids): + ICP = self.env["ir.config_parameter"].sudo() + cache_invalidation_enable = ICP.get_param("vsf_cache_invalidation", False) + + if not cache_invalidation_enable: + return False + + for res_id in res_ids: + if not self.find_invalidate_cache(res_model, res_id): + query = """ + INSERT INTO + invalidate_cache( + res_model, + res_id, + create_date, + write_date, + create_uid, + write_uid + ) + VALUES(%s, %s, %s, %s, %s, %s); + """ + now = datetime.now() + uid = self.env.user.id + params = ( + res_model, + res_id, + now, + now, + uid, + uid, + ) + + self.env.cr.execute(query, params) + + @api.model + def delete_invalidate_cache(self, ids): + query = """ + DELETE FROM invalidate_cache + WHERE id IN %s + """ + self.env.cr.execute(query, (tuple(ids),)) + + @api.model + def request_cache_invalidation(self, url, key, tags): + if url and key and tags: + try: + requests.get(url, params={"key": key, "tags": tags}, timeout=5) + except Exception as e: + _logger.error(e) + self.env.cr.rollback() + + @api.model + def request_vsf_cache_invalidation(self): + ICP = self.env["ir.config_parameter"].sudo() + url = ICP.get_param("vsf_cache_invalidation_url", False) + key = ICP.get_param("vsf_cache_invalidation_key", False) + + models = [ + { + "name": "product.template", + "tags_method": "_get_product_tags", + }, + { + "name": "product.public.category", + "tags_method": "_get_category_tags", + }, + ] + + for model in models: + invalidate_caches = self.env["invalidate.cache"].search( + [("res_model", "=", model["name"])] + ) + if invalidate_caches: + res_ids = invalidate_caches.mapped("res_id") + tags = getattr(self, model["tags_method"])(res_ids) + self.delete_invalidate_cache(invalidate_caches.ids) + self.request_cache_invalidation(url, key, tags) + # FIXME: this probably should not be done. + self.env.cr.commit() # pylint: disable=invalid-commit + + # TODO: these methods don't make sense. They have same code. Need + # to redesign this, when its clear what they actually should return. + def _get_product_tags(self, product_ids): + tags = ",".join(f"P{product_id}" for product_id in product_ids) + categories = ( + self.env["product.template"] + .search([("id", "in", product_ids)]) + .mapped("public_categ_ids") + ) + ancestor_categs = self.env["product.public.category"] + for parent in categories.get_ancestors(): + ancestor_categs |= parent + categories |= ancestor_categs + if categories: + tags += "," + ",".join(f"C{category_id}" for category_id in categories.ids) + return tags + + # TODO: this method most likely is incorrect. Should not expect product_ids as + # it looks like when its called, it actually calls with category_ids + # (product.public.category). + def _get_category_tags(self, product_ids): + tags = ",".join(f"P{product_id}" for product_id in product_ids) + categories = ( + self.env["product.template"] + .search([("id", "in", product_ids)]) + .mapped("public_categ_ids") + ) + ancestor_categs = self.env["product.public.category"] + for parent in categories.get_ancestors(): + ancestor_categs |= parent + categories |= ancestor_categs + if categories: + tags += "," + ",".join(f"C{category_id}" for category_id in categories.ids) + return tags diff --git a/vuestorefront/models/ir_http.py b/vuestorefront/models/ir_http.py new file mode 100644 index 0000000..903fed7 --- /dev/null +++ b/vuestorefront/models/ir_http.py @@ -0,0 +1,162 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +import codecs +import io + +from PIL.WebPImagePlugin import Image + +from odoo import api, http, models +from odoo.http import request +from odoo.tools import image_process +from odoo.tools.safe_eval import safe_eval + + +class Http(models.AbstractModel): + _inherit = "ir.http" + + @api.model + def _content_image( + self, + xmlid=None, + model="ir.attachment", + res_id=None, + field="datas", + filename_field="name", + unique=None, + filename=None, + mimetype=None, + download=None, + width=0, + height=0, + crop=False, + quality=0, + access_token=None, + **kwargs, + ): + if filename and filename.endswith(("jpeg", "jpg")): + request.image_format = "jpeg" + + return super()._content_image( + xmlid=xmlid, + model=model, + res_id=res_id, + field=field, + filename_field=filename_field, + unique=unique, + filename=filename, + mimetype=mimetype, + download=download, + width=width, + height=height, + crop=crop, + quality=quality, + access_token=access_token, + **kwargs, + ) + + @api.model + def _content_image_get_response( + self, + status, + headers, + image_base64, + model="ir.attachment", + field="datas", + download=None, + width=0, + height=0, + crop=False, + quality=0, + ): + """Handle image for content. + + Center image in background with color, resize, compress and convert + image to webp or jpeg. + """ + if status == 200 and image_base64 and width and height: + try: + # Accepts jpeg and webp, defaults to webp if none found + if hasattr(request, "image_format"): + image_format = request.image_format + else: + image_format = "webp" + + width = int(width) + height = int(height) + ICP = request.env["ir.config_parameter"].sudo() + + image_base64 = image_process(image_base64, size=(width, height)) + img = Image.open(io.BytesIO(codecs.decode(image_base64, "base64"))) + if img.mode != "RGBA": + img = img.convert("RGBA") + + # Get background color from settings + try: + background_rgba = safe_eval( + ICP.get_param( + "vsf_image_background_rgba", "(255, 255, 255, 255)" + ) + ) + # FIXME: this should handle only specific exceptions, not + # all. + except Exception: + background_rgba = (255, 255, 255, 255) + + # Create a new background, merge the background with the image centered + img_w, img_h = img.size + if image_format == "jpeg": + background = Image.new("RGB", (width, height), background_rgba[:3]) + else: + background = Image.new("RGBA", (width, height), background_rgba) + bg_w, bg_h = background.size + offset = ((bg_w - img_w) // 2, (bg_h - img_h) // 2) + background.paste(img, offset) + + # Get compression quality from settings + quality = ICP.get_param("vsf_image_quality", 100) + + stream = io.BytesIO() + if image_format == "jpeg": + background.save(stream, format=image_format.upper(), subsampling=0) + else: + background.save( + stream, + format=image_format.upper(), + quality=quality, + subsampling=0, + ) + image_base64 = base64.b64encode(stream.getvalue()) + + except Exception: + return request.not_found() + + # Replace Content-Type by generating a new list of headers + new_headers = [] + for header in headers: + if header[0] == "Content-Type": + new_headers.append(("Content-Type", f"image/{image_format}")) + else: + new_headers.append(header) + + # Response + content = base64.b64decode(image_base64) + new_headers = http.set_safe_image_headers(new_headers, content) + response = request.make_response(content, new_headers) + response.status_code = status + return response + + # Fallback to super function + return super()._content_image_get_response( + status, + headers, + image_base64, + model=model, + field=field, + download=download, + width=width, + height=height, + crop=crop, + quality=quality, + ) diff --git a/graphql_vuestorefront/models/payment_transaction.py b/vuestorefront/models/payment_transaction.py similarity index 50% rename from graphql_vuestorefront/models/payment_transaction.py rename to vuestorefront/models/payment_transaction.py index 97dfd6f..85e21fb 100644 --- a/graphql_vuestorefront/models/payment_transaction.py +++ b/vuestorefront/models/payment_transaction.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import models, api, fields, tools, _ +from odoo import fields, models class PaymentTransactionInherit(models.Model): - _inherit = 'payment.transaction' + _inherit = "payment.transaction" - created_on_vsf = fields.Boolean(string='Created on Vsf?', default=False) + created_on_vsf = fields.Boolean(string="Created on Vsf?", default=False) diff --git a/vuestorefront/models/product_product.py b/vuestorefront/models/product_product.py new file mode 100644 index 0000000..912d61e --- /dev/null +++ b/vuestorefront/models/product_product.py @@ -0,0 +1,58 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json + +from odoo import models + +from odoo.addons.http_routing.models.ir_http import slug + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def get_json_ld(self): + self.ensure_one() + if self.json_ld: + return self.json_ld + + env = self.env + website = env["website"].get_current_website() + # TODO: reduce boilerplate + base_url = env["ir.config_parameter"].sudo().get_param("web.base.url", "") + if base_url and base_url[-1:] == "/": + base_url = base_url[:-1] + + # Get list of images + images = list() + if self.image_1920: + images.append("%s/web/image/product.product/%s/image" % (base_url, self.id)) + + json_ld = { + "@context": "https://schema.org/", + "@type": "Product", + "name": self.display_name, + "image": images, + "offers": { + "@type": "Offer", + "url": "%s/product/%s" % (website.domain or "", slug(self)), + "priceCurrency": self.currency_id.name, + "price": self.list_price, + "itemCondition": "https://schema.org/NewCondition", + "availability": "https://schema.org/InStock", + "seller": { + "@type": "Organization", + "name": website + and website.display_name + or self.env.user.company_id.display_name, + }, + }, + } + + if self.description_sale: + json_ld.update({"description": self.description_sale}) + + if self.default_code: + json_ld.update({"sku": self.default_code}) + + return json.dumps(json_ld) diff --git a/vuestorefront/models/product_public_category.py b/vuestorefront/models/product_public_category.py new file mode 100644 index 0000000..e00db00 --- /dev/null +++ b/vuestorefront/models/product_public_category.py @@ -0,0 +1,78 @@ +import json + +from odoo import api, fields, models + +from ..utils import slugify_with_sfx + + +class ProductPublicCategory(models.Model): + _inherit = "product.public.category" + + slug = fields.Char( + readonly=False, + index=True, + store=True, + compute="_compute_slug", + ) + json_ld = fields.Char("JSON-LD") + + _sql_constraints = [ + ( + "slug_uniq", + "unique (slug)", + "The Website Slug must be unique!", + ) + ] + + @api.depends("name") + def _compute_slug(self): + for rec in self: + if not isinstance(rec.id, models.NewId) and not rec.slug and rec.name: + rec.slug = slugify_with_sfx(rec.name, f"-{rec.id}") + + def write(self, vals): + res = super().write(vals) + self.env["invalidate.cache"].create_invalidate_cache(self._name, self.ids) + return res + + def unlink(self): + self.env["invalidate.cache"].create_invalidate_cache(self._name, self.ids) + return super().unlink() + + def get_json_ld(self): + self.ensure_one() + if self.json_ld: + return self.json_ld + + website = self.env["website"].get_current_website() + base_url = website.domain or "" + if base_url and base_url[-1] == "/": + base_url = base_url[:-1] + + json_ld = { + "@context": "https://schema.org", + "@type": "CollectionPage", + "url": "{}{}".format(base_url, self.slug or ""), + "name": self.display_name, + } + + return json.dumps(json_ld) + + def get_ancestors(self): + for categ in self: + parent = categ.parent_id + while parent: + yield parent + parent = parent.parent_id + + def get_category_by_slug(self, slug): + return self.search([("slug", "=", slug)]) + + @api.model + def _get_category_slug_leaf(self, category_slug): + # We need to search for category, because we want to include child + # categories. + categ = self.get_category_by_slug(category_slug) + if categ: + return ("public_categ_ids", "child_of", categ.id) + return None diff --git a/vuestorefront/models/product_template.py b/vuestorefront/models/product_template.py new file mode 100644 index 0000000..7317d01 --- /dev/null +++ b/vuestorefront/models/product_template.py @@ -0,0 +1,216 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json + +from odoo import api, fields, models +from odoo.osv import expression +from odoo.tools.sql import column_exists, create_column + +from odoo.addons.http_routing.models.ir_http import slug + +from ..utils import slugify_with_sfx + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def _auto_init(self): + """Create/set slug via SQL to save on memory usage.""" + cr = self.env.cr + if not column_exists(cr, "product_template", "slug"): + create_column(cr, "product_template", "slug", "varchar") + cr.execute("SELECT id, name FROM product_template WHERE name IS NOT NULL") + for res in cr.fetchall(): + id_, name = res + cr.execute( + "UPDATE product_template SET slug = %s WHERE id = %s", + (slugify_with_sfx(name, f"-{id_}"), id_), + ) + return super()._auto_init() + + @api.depends("name") + def _compute_slug(self): + for product in self: + # To make sure we don't assign ID before record is created. + if not isinstance(product.id, models.NewId): + product.slug = slugify_with_sfx(product.name, f"-{product.id}") + + slug = fields.Char( + compute="_compute_slug", + store=True, + readonly=True, + ) + json_ld = fields.Char("JSON-LD") + + @api.model + def prepare_vsf_domain(self, search, **kwargs): + # Only get sellable products. + domains = [self.env["website"].get_current_website().sale_product_domain()] + if self.is_vsf_published_only(**kwargs): + domains.append([("is_published", "=", True)]) + # Filter with ids + if kwargs.get("ids", False): + domains.append([("id", "in", kwargs["ids"])]) + # Filter with Category ID + if kwargs.get("category_id", False): + domains.append([("public_categ_ids", "child_of", kwargs["category_id"])]) + if kwargs.get("category_slug"): + category_slug_leaf = self.env[ + "product.public.category" + ]._get_category_slug_leaf(kwargs["category_slug"]) + if category_slug_leaf is not None: + domains.append([category_slug_leaf]) + # Filter With Barcode + if kwargs.get("barcode", False): + domains.append([("barcode", "ilike", kwargs["barcode"])]) + # Filter With Name + if kwargs.get("name", False): + name = kwargs["name"] + for n in name.split(" "): + domains.append([("name", "ilike", n)]) + if search: + for name in search.split(" "): + domains.append(self._get_product_search_name_domain(name)) + # Product Price Filter + if kwargs.get("min_price", False): + domains.append([("list_price", ">=", float(kwargs["min_price"]))]) + if kwargs.get("max_price", False): + domains.append([("list_price", "<=", float(kwargs["max_price"]))]) + # Deprecated: filter with Attribute Value + if kwargs.get("attribute_value_id", False): + domains.append( + [("attribute_line_ids.value_ids", "in", kwargs["attribute_value_id"])] + ) + # Filter with Attribute Value + if kwargs.get("attrib_values", False): + ids = [] + + for value in kwargs["attrib_values"]: + try: + value = value.split("-") + if len(value) != 2: + continue + + attribute_value_id = int(value[1]) + except ValueError: + continue + + ids.append(attribute_value_id) + if ids: + domains.append([("attribute_line_ids.value_ids", "in", ids)]) + return expression.AND(domains) + + def write(self, vals): + res = super().write(vals) + self.env["invalidate.cache"].create_invalidate_cache(self._name, self.ids) + return res + + def unlink(self): + self.env["invalidate.cache"].create_invalidate_cache(self._name, self.ids) + return super().unlink() + + def _get_combination_info( + self, + combination=False, + product_id=False, + add_qty=1, + pricelist=False, + parent_combination=False, + only_template=False, + ): + """Add discount value and percentage based.""" + combination_info = super()._get_combination_info( + combination=combination, + product_id=product_id, + add_qty=add_qty, + pricelist=pricelist, + parent_combination=parent_combination, + only_template=only_template, + ) + + discount = 0.00 + discount_perc = 0 + if combination_info["has_discounted_price"] and product_id: + discount = combination_info["list_price"] - combination_info["price"] + discount_perc = ( + combination_info["list_price"] + and (discount * 100 / combination_info["list_price"]) + or 0 + ) + + if discount_perc: + discount_perc = int(round(discount_perc, 0)) + if not discount_perc: + discount_perc = 1 + + combination_info.update( + { + "discount": round(discount, 2), + "discount_perc": discount_perc, + } + ) + + return combination_info + + def get_json_ld(self): + self.ensure_one() + if self.json_ld: + return self.json_ld + + env = self.env + website = env["website"].get_current_website() + base_url = env["ir.config_parameter"].sudo().get_param("web.base.url", "") + if base_url and base_url[-1:] == "/": + base_url = base_url[:-1] + + # Get list of images + images = list() + if self.image_1920: + images.append("%s/web/image/product.product/%s/image" % (base_url, self.id)) + + json_ld = { + "@context": "https://schema.org/", + "@type": "Product", + "name": self.display_name, + "image": images, + "offers": { + "@type": "Offer", + "url": "%s/product/%s" % (website.domain or "", slug(self)), + "priceCurrency": self.currency_id.name, + "price": self.list_price, + "itemCondition": "https://schema.org/NewCondition", + "availability": "https://schema.org/InStock", + "seller": { + "@type": "Organization", + "name": website + and website.display_name + or self.env.user.company_id.display_name, + }, + }, + } + + if self.description_sale: + json_ld.update({"description": self.description_sale}) + + if self.default_code: + json_ld.update({"sku": self.default_code}) + + return json.dumps(json_ld) + + @api.model + def is_vsf_published_only(self, **kwargs): + """Check if product search is for published products only.""" + return True + + @api.model + def _get_product_search_name_domain(self, name): + return [ + "|", + "|", + "|", + ("name", "ilike", name), + ("description_sale", "like", name), + ("default_code", "like", name), + ("barcode", "ilike", name), + ] diff --git a/vuestorefront/models/res_config_settings.py b/vuestorefront/models/res_config_settings.py new file mode 100644 index 0000000..ad02583 --- /dev/null +++ b/vuestorefront/models/res_config_settings.py @@ -0,0 +1,83 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import uuid + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + vsf_debug_mode = fields.Boolean("Debug Mode") + vsf_payment_success_return_url = fields.Char( + "Payment Success Return Url", + related="website_id.vsf_payment_success_return_url", + readonly=False, + required=True, + ) + vsf_payment_error_return_url = fields.Char( + "Payment Error Return Url", + related="website_id.vsf_payment_error_return_url", + readonly=False, + required=True, + ) + vsf_cache_invalidation = fields.Boolean("Cache Invalidation") + vsf_cache_invalidation_key = fields.Char("Cache Invalidation Key", required=True) + vsf_cache_invalidation_url = fields.Char("Cache Invalidation Url", required=True) + vsf_mailing_list_id = fields.Many2one( + "mailing.list", + "Newsletter", + domain=[("is_public", "=", True)], + related="website_id.vsf_mailing_list_id", + readonly=False, + required=True, + ) + + # VSF Images + vsf_image_quality = fields.Integer("Vue Storefront Image Quality", required=True) + vsf_image_background_rgba = fields.Char("Background RGBA", required=True) + vsf_image_resize_limit = fields.Integer( + "Resize Limit", + required=True, + help="Limit in pixels to resize image for width and height", + ) + + def get_values(self): + res = super().get_values() + ICP = self.env["ir.config_parameter"].sudo() + res.update( + vsf_debug_mode=ICP.get_param("vsf_debug_mode"), + vsf_cache_invalidation=ICP.get_param("vsf_cache_invalidation"), + vsf_cache_invalidation_key=ICP.get_param("vsf_cache_invalidation_key"), + vsf_cache_invalidation_url=ICP.get_param("vsf_cache_invalidation_url"), + vsf_image_quality=int(ICP.get_param("vsf_image_quality", 100)), + vsf_image_background_rgba=ICP.get_param( + "vsf_image_background_rgba", "(255, 255, 255, 255)" + ), + vsf_image_resize_limit=int(ICP.get_param("vsf_image_resize_limit", 1920)), + ) + return res + + def set_values(self): + if self.vsf_image_quality < 0 or self.vsf_image_quality > 100: + raise ValidationError(_("Invalid image quality percentage.")) + + if self.vsf_image_resize_limit < 0: + raise ValidationError(_("Invalid image resize limit.")) + + super().set_values() + ICP = self.env["ir.config_parameter"].sudo() + ICP.set_param("vsf_debug_mode", self.vsf_debug_mode) + ICP.set_param("vsf_cache_invalidation", self.vsf_cache_invalidation) + ICP.set_param("vsf_cache_invalidation_key", self.vsf_cache_invalidation_key) + ICP.set_param("vsf_cache_invalidation_url", self.vsf_cache_invalidation_url) + ICP.set_param("vsf_image_quality", self.vsf_image_quality) + ICP.set_param("vsf_image_background_rgba", self.vsf_image_background_rgba) + ICP.set_param("vsf_image_resize_limit", self.vsf_image_resize_limit) + + @api.model + def create_vsf_cache_invalidation_key(self): + ICP = self.env["ir.config_parameter"].sudo() + ICP.set_param("vsf_cache_invalidation_key", str(uuid.uuid4())) diff --git a/vuestorefront/models/res_partner.py b/vuestorefront/models/res_partner.py new file mode 100644 index 0000000..ef341d6 --- /dev/null +++ b/vuestorefront/models/res_partner.py @@ -0,0 +1,52 @@ +from odoo import api, models +from odoo.osv import expression + +VSF_ADDRESS_DIRECT_FIELDS = ["name", ""] + + +class ResPartner(models.Model): + _inherit = "res.partner" + + @api.model + def prepare_vsf_address_direct_fields(self): + return [ + "name", + "street", + "street2", + "phone", + "zip", + "city", + "state_id", + "country_id", + "email", + ] + + @api.model + def prepare_vsf_address_vals(self, address): + vals = {} + for fname in self.prepare_vsf_address_direct_fields(): + if fname in address: + vals[fname] = address[fname] + return vals + + def prepare_vsf_invoice_address_domain(self): + self.ensure_one() + return [("type", "=", "invoice")] + + def prepare_vsf_delivery_address_domain(self): + self.ensure_one() + return [("type", "=", "delivery")] + + def _prepare_vsf_base_address_domain(self): + self.ensure_one() + return [("id", "child_of", self.commercial_partner_id.ids)] + + def _prepare_vsf_address_domain(self, address_types): + self.ensure_one() + base_domain = self._prepare_vsf_base_address_domain() + types = [at.value for at in address_types] + type_domains = [] + for addr_type in types: + method_name = f"prepare_vsf_{addr_type}_address_domain" + type_domains.append(getattr(self, method_name)()) + return expression.AND([base_domain, expression.OR(type_domains)]) diff --git a/vuestorefront/models/res_users.py b/vuestorefront/models/res_users.py new file mode 100644 index 0000000..15abde1 --- /dev/null +++ b/vuestorefront/models/res_users.py @@ -0,0 +1,81 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import _, api, models +from odoo.exceptions import UserError +from odoo.http import request + +from odoo.addons.auth_signup.models.res_partner import now + +_logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = "res.users" + + def api_action_reset_password(self): + """Create signup token for each user, and send their signup url by email.""" + if self.filtered(lambda user: not user.active): + raise UserError(_("You cannot perform this action on an archived user.")) + # prepare reset password signup + create_mode = bool(self.env.context.get("create_user")) + + # no time limit for initial invitation, only for reset password + expiration = False if create_mode else now(days=+1) + + self.mapped("partner_id").signup_prepare( + signup_type="reset", expiration=expiration + ) + + # send email to users with their signup url + template = self.env.ref("vuestorefront.website_reset_password_email") + + assert template._name == "mail.template" + + website = request.env["website"].get_current_website() + domain = website.domain or "" + if domain and domain[-1] == "/": + domain = domain[:-1] + + email_values = { + "email_cc": False, + "auto_delete": True, + "recipient_ids": [], + "partner_ids": [], + "scheduled_date": False, + } + + for user in self: + token = user.signup_token + signup_url = "%s/forgot-password/new-password?token=%s" % (domain, token) + if not user.email: + raise UserError( + _("Cannot send email: user %s has no email address.", user.name) + ) + email_values["email_to"] = user.email + with self.env.cr.savepoint(): + force_send = not create_mode + template.with_context(lang=user.lang, signup_url=signup_url).send_mail( + user.id, + force_send=force_send, + raise_exception=True, + email_values=email_values, + ) + _logger.info( + "Password reset email sent for user <%s> to <%s>", + user.login, + user.email, + ) + + @api.model + def prepare_vsf_signup_vals(self, name, login, password, **kw): + vals = { + "name": name, + "login": login, + "password": password, + } + if "vat" in kw: + vals["vat"] = kw["vat"] + return vals diff --git a/vuestorefront/models/sale_order.py b/vuestorefront/models/sale_order.py new file mode 100644 index 0000000..6b865fd --- /dev/null +++ b/vuestorefront/models/sale_order.py @@ -0,0 +1,13 @@ +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def prepare_vsf_vals(self, data): + # Only used for current session user current order. + self.ensure_one() + vals = {} + if "client_order_ref" in data: + vals["client_order_ref"] = data["client_order_ref"] + return vals diff --git a/vuestorefront/models/website.py b/vuestorefront/models/website.py new file mode 100644 index 0000000..3295d59 --- /dev/null +++ b/vuestorefront/models/website.py @@ -0,0 +1,98 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +import requests + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class Website(models.Model): + _inherit = "website" + + vsf_payment_success_return_url = fields.Char( + "Payment Success Return Url", required=True, translate=True, default="Dummy" + ) + vsf_payment_error_return_url = fields.Char( + "Payment Error Return Url", required=True, translate=True, default="Dummy" + ) + vsf_mailing_list_id = fields.Many2one( + "mailing.list", "Newsletter", domain=[("is_public", "=", True)] + ) + + @api.model + def enable_b2c_reset_password(self): + """Enable sign up and reset password on default website.""" + website = self.env.ref("website.default_website", raise_if_not_found=False) + if website: + website.auth_signup_uninvited = "b2c" + + ICP = self.env["ir.config_parameter"].sudo() + ICP.set_param("auth_signup.invitation_scope", "b2c") + ICP.set_param("auth_signup.reset_password", True) + + +class WebsiteRewrite(models.Model): + _inherit = "website.rewrite" + + def _get_vsf_tags(self): + tags = "WR%s" % self.id + return tags + + def _vsf_request_cache_invalidation(self): + ICP = self.env["ir.config_parameter"].sudo() + url = ICP.get_param("vsf_cache_invalidation_url", False) + key = ICP.get_param("vsf_cache_invalidation_key", False) + + if url and key: + try: + for website_rewrite in self: + tags = website_rewrite._get_vsf_tags() + + # Make the GET request to the /cache-invalidate + requests.get(url, params={"key": key, "tags": tags}, timeout=5) + except Exception: + _logger.error( + "Something went wrong processing cache invalidation", exc_info=True + ) + + def write(self, vals): + res = super().write(vals) + self._vsf_request_cache_invalidation() + return res + + def unlink(self): + self._vsf_request_cache_invalidation() + return super().unlink() + + +class WebsiteMenu(models.Model): + _inherit = "website.menu" + + is_footer = fields.Boolean(default=False) + menu_image_ids = fields.One2many( + "website.menu.image", "menu_id", string="Menu Images" + ) + is_mega_menu = fields.Boolean(store=True) + + +class WebsiteMenuImage(models.Model): + _name = "website.menu.image" + _description = "Website Menu Image" + + def _default_sequence(self): + menu = self.search([], limit=1, order="sequence DESC") + return menu.sequence or 0 + + menu_id = fields.Many2one("website.menu", "Website Menu", required=True) + sequence = fields.Integer(default=_default_sequence) + image = fields.Image(required=True) + tag = fields.Char() + title = fields.Char() + subtitle = fields.Char() + text_color = fields.Char("Text Color (Hex)", help="#111000") + button_text = fields.Char() + button_url = fields.Char("Button URL") diff --git a/graphql_vuestorefront/schema.py b/vuestorefront/schema.py similarity index 65% rename from graphql_vuestorefront/schema.py rename to vuestorefront/schema.py index 4d3e13f..fa0ad26 100644 --- a/graphql_vuestorefront/schema.py +++ b/vuestorefront/schema.py @@ -1,15 +1,25 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from odoo.addons.graphql_base import OdooObjectType -from odoo.addons.graphql_vuestorefront.schemas import ( - country, category, product, order, - invoice, contact_us, user_profile, sign, - address, wishlist, shop, payment, - mailing_list, website, + +from .schemas import ( + address, + category, + contact_us, + country, + invoice, + mailing_list, + order, + payment, + product, + shop, + sign, + user_profile, + website, + wishlist, ) @@ -51,7 +61,16 @@ class Mutation( schema = graphene.Schema( query=Query, mutation=Mutation, - types=[country.CountryList, category.CategoryList, product.ProductList, product.ProductVariantData, order.OrderList, - invoice.InvoiceList, wishlist.WishlistData, shop.CartData, mailing_list.MailingContactList, - mailing_list.MailingListList] + types=[ + country.CountryList, + category.CategoryList, + product.ProductList, + product.ProductVariantData, + order.OrderList, + invoice.InvoiceList, + wishlist.WishlistData, + shop.CartData, + mailing_list.MailingContactList, + mailing_list.MailingListList, + ], ) diff --git a/graphql_vuestorefront/schemas/__init__.py b/vuestorefront/schemas/__init__.py similarity index 94% rename from graphql_vuestorefront/schemas/__init__.py rename to vuestorefront/schemas/__init__.py index 0c2cbef..ea3806d 100644 --- a/graphql_vuestorefront/schemas/__init__.py +++ b/vuestorefront/schemas/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/graphql_vuestorefront/schemas/address.py b/vuestorefront/schemas/address.py similarity index 53% rename from graphql_vuestorefront/schemas/address.py rename to vuestorefront/schemas/address.py index 8af7111..d20ec3f 100644 --- a/graphql_vuestorefront/schemas/address.py +++ b/vuestorefront/schemas/address.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). @@ -7,85 +6,90 @@ from odoo import _ from odoo.http import request -from odoo.addons.graphql_vuestorefront.schemas.objects import Partner + +from .objects import Partner def get_partner(env, partner_id, order, website): if not order: - raise GraphQLError(_('Shopping cart not found.')) + raise GraphQLError(_("Shopping cart not found.")) - ResPartner = env['res.partner'].with_context(show_address=1).sudo() + ResPartner = env["res.partner"].with_context(show_address=1).sudo() partner = ResPartner.browse(partner_id) # Is public user - if not order.partner_id.user_ids or order.partner_id.id == website.user_id.sudo().partner_id.id: + if ( + not order.partner_id.user_ids + or order.partner_id.id == website.user_id.sudo().partner_id.id + ): partner_id = order.partner_id.id else: partner_id = env.user.partner_id.commercial_partner_id.id # Addresses that belong to this user - shippings = ResPartner.search([ - ("id", "child_of", partner_id), - '|', ("type", "in", ["delivery", "invoice"]), - ("id", "=", partner_id) - ]) + shippings = ResPartner.search( + [ + ("id", "child_of", partner_id), + "|", + ("type", "in", ["delivery", "invoice"]), + ("id", "=", partner_id), + ] + ) # Validate if the address exists and if the user has access to this address if not partner or not partner.exists() or partner.id not in shippings.ids: - raise GraphQLError(_('Address not found.')) + raise GraphQLError(_("Address not found.")) return partner class AddressEnum(graphene.Enum): - Billing = 'invoice' - Shipping = 'delivery' + Billing = "invoice" + Shipping = "delivery" class AddressFilterInput(graphene.InputObjectType): - address_type = graphene.List(AddressEnum) + address_types = graphene.List(AddressEnum) class AddressQuery(graphene.ObjectType): addresses = graphene.List( graphene.NonNull(Partner), - filter=graphene.Argument(AddressFilterInput, default_value={}) + filter=graphene.Argument(AddressFilterInput, default_value={}), ) @staticmethod def resolve_addresses(self, info, filter): env = info.context["env"] - ResPartner = env['res.partner'].with_context(show_address=1).sudo() - website = env['website'].get_current_website() + ResPartner = env["res.partner"].with_context(show_address=1).sudo() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() - if not order: - raise GraphQLError(_('Shopping cart not found.')) + raise GraphQLError(_("Shopping cart not found.")) + # TODO: getting partner ID should be on its own function/method. # Is public user - if not order.partner_id.user_ids or order.partner_id.id == website.user_id.sudo().partner_id.id: + if ( + not order.partner_id.user_ids + or order.partner_id.id == website.user_id.sudo().partner_id.id + ): partner_id = order.partner_id.id else: partner_id = env.user.partner_id.commercial_partner_id.id - + partner = env["res.partner"].browse(partner_id) # Get all addresses of a specific addressType - delivery or/and shipping - if filter.get('address_type'): - types = [address_type.value for address_type in filter.get('address_type', [])] - - domain = [ - ("id", "child_of", partner_id), - ("type", "in", types), - ] + if filter.get("address_types"): + domain = partner._prepare_vsf_address_domain(filter["address_types"]) # Get all addresses with addressType delivery and invoice else: domain = [ ("id", "child_of", partner_id), - '|', ("type", "in", ['delivery', 'invoice']), + "|", + ("type", "in", ["delivery", "invoice"]), ("id", "=", partner_id), ] - - return ResPartner.search(domain, order='id desc') + return ResPartner.search(domain, order="id desc") class AddAddressInput(graphene.InputObjectType): @@ -123,57 +127,40 @@ class UpdateAddressInput(graphene.InputObjectType): class AddAddress(graphene.Mutation): class Arguments: - type = AddressEnum(required=True) + address_type = AddressEnum(required=True) address = AddAddressInput() Output = Partner @staticmethod - def mutate(self, info, type, address): + def mutate(self, info, address_type, address): env = info.context["env"] - ResPartner = env['res.partner'].sudo().with_context(tracking_disable=True) - website = env['website'].get_current_website() + ResPartner = env["res.partner"].sudo().with_context(tracking_disable=True) + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() - if not order: - raise GraphQLError(_('Shopping cart not found.')) - - values = { - 'name': address.get('name'), - 'street': address.get('street'), - 'street2': address.get('street2'), - 'phone': address.get('phone'), - 'zip': address.get('zip'), - 'city': address.get('city'), - 'state_id': address.get('state_id', False), - 'country_id': address.get('country_id', False), - 'email': address.get('email', False), - } + raise GraphQLError(_("Shopping cart not found.")) + values = ResPartner.prepare_vsf_address_vals(address) partner_id = order.partner_id.id - # Check public user if partner_id == website.user_id.sudo().partner_id.id: # Create main contact - values['type'] = 'contact' - partner_id = ResPartner.create(values).id + partner_id = ResPartner.create(dict(values, type="contact")).id order.partner_id = partner_id - - values['type'] = type.value - values['parent_id'] = partner_id - + values.update({"type": address_type.value, "parent_id": partner_id}) # Create the new shipping or invoice address partner = ResPartner.create(values) # Update order with the new shipping or invoice address - if values['type'] == 'invoice': + if values["type"] == "invoice": order.partner_invoice_id = partner.id - elif values['type'] == 'delivery': + elif values["type"] == "delivery": order.partner_shipping_id = partner.id - - # Trigger the change of fiscal position when the shipping address is modified - order._compute_fiscal_position_id() + # Trigger the change of fiscal position when the shipping address + # is modified + order._compute_fiscal_position_id() return partner @@ -187,39 +174,16 @@ class Arguments: @staticmethod def mutate(self, info, address): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() - - partner = get_partner(env, address['id'], order, website) - - values = {} - if address.get('name'): - values.update({'name': address['name']}) - if address.get('street'): - values.update({'street': address['street']}) - if address.get('street2'): - values.update({'street2': address['street2']}) - if address.get('phone'): - values.update({'phone': address['phone']}) - if address.get('zip'): - values.update({'zip': address['zip']}) - if address.get('city'): - values.update({'city': address['city']}) - if address.get('state_id'): - values.update({'state_id': address['state_id']}) - if address.get('country_id'): - values.update({'country_id': address['country_id']}) - - # Trigger the change of fiscal position when the shipping address is modified - order._compute_fiscal_position_id() - - if address.get('email'): - values.update({'email': address['email']}) - + partner = get_partner(env, address["id"], order, website) + values = env["res.partner"].prepare_vsf_address_vals(address) + # Trigger the change of fiscal position when the shipping address + # is modified + order._compute_fiscal_position_id() if values: partner.write(values) - return partner @@ -232,11 +196,11 @@ class Arguments: @staticmethod def mutate(self, info, address): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() - partner = get_partner(env, address['id'], order, website) + partner = get_partner(env, address["id"], order, website) if not partner.parent_id: raise GraphQLError(_("You can't delete the primary address.")) @@ -246,48 +210,54 @@ def mutate(self, info, address): if order.partner_shipping_id.id == partner.id: order.partner_shipping_id = partner.parent_id.id - - # Archive address, safer than delete since this address could be in use by other object + order._compute_fiscal_position_id() + # Archive address, safer than delete since this address could be in use by + # other object partner.active = False - - # Trigger the change of fiscal position when the shipping address is modified - order._compute_fiscal_position_id() - return DeleteAddress(result=True) class SelectAddress(graphene.Mutation): class Arguments: - type = AddressEnum(required=True) + address_type = AddressEnum(required=True) address = SelectAddressInput() Output = Partner @staticmethod - def mutate(self, info, type, address): + def mutate(self, info, address_type, address): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() - partner = get_partner(env, address['id'], order, website) + partner = get_partner(env, address["id"], order, website) # Update order with the new shipping or invoice address - if type.value == 'invoice': + if address_type.value == "invoice": order.partner_invoice_id = partner.id - elif type.value == 'delivery': + elif address_type.value == "delivery": order.partner_shipping_id = partner.id - - # Trigger the change of fiscal position when the shipping address is modified - order._compute_fiscal_position_id() + # Trigger the change of fiscal position when the shipping address + # is modified + order._compute_fiscal_position_id() return partner class AddressMutation(graphene.ObjectType): - add_address = AddAddress.Field(description='Add new billing or shipping address and set it on the shopping cart.') - update_address = UpdateAddress.Field(description="Update a billing or shipping address and set it on the shopping " - "cart.") - delete_address = DeleteAddress.Field(description='Delete a billing or shipping address.') - select_address = SelectAddress.Field(description="Select a billing or shipping address to be used on the shopping " - "cart.") + add_address = AddAddress.Field( + description="Add new billing or shipping address and set it on the " + + "shopping cart." + ) + update_address = UpdateAddress.Field( + description="Update a billing or shipping address and " # nosec: B608 + + "set it on the shopping cart." + ) + delete_address = DeleteAddress.Field( + description="Delete a billing or shipping address." + ) + select_address = SelectAddress.Field( + description="Select a billing or shipping address to be used on the " + + "shopping cart." + ) diff --git a/graphql_vuestorefront/schemas/category.py b/vuestorefront/schemas/category.py similarity index 65% rename from graphql_vuestorefront/schemas/category.py rename to vuestorefront/schemas/category.py index 94b997a..7156d7d 100644 --- a/graphql_vuestorefront/schemas/category.py +++ b/vuestorefront/schemas/category.py @@ -1,29 +1,27 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene -from odoo.addons.graphql_vuestorefront.schemas.objects import ( - SortEnum, Category -) +from ..utils import get_offset +from .objects import Category, SortEnum def get_search_order(sort): - sorting = '' + sorting = "" for field, val in sort.items(): if sorting: - sorting += ', ' - sorting += '%s %s' % (field, val.value) + sorting += ", " + sorting += "%s %s" % (field, val.value) if not sorting: - sorting = 'sequence ASC, id ASC' + sorting = "sequence ASC, id ASC" return sorting class CategoryFilterInput(graphene.InputObjectType): - id = graphene.List(graphene.Int) + ids = graphene.List(graphene.Int) parent = graphene.Boolean() @@ -53,52 +51,41 @@ class CategoryQuery(graphene.ObjectType): current_page=graphene.Int(default_value=1), page_size=graphene.Int(default_value=20), search=graphene.String(default_value=False), - sort=graphene.Argument(CategorySortInput, default_value={}) + sort=graphene.Argument(CategorySortInput, default_value={}), ) @staticmethod def resolve_category(self, info, id=None, slug=None): - env = info.context['env'] - Category = env['product.public.category'] - - domain = env['website'].get_current_website().website_domain() - + env = info.context["env"] + Category = env["product.public.category"] + domain = env["website"].get_current_website().website_domain() if id: - domain += [('id', '=', id)] + domain += [("id", "=", id)] category = Category.search(domain, limit=1) elif slug: - domain += [('website_slug', '=', slug)] + domain += [("slug", "=", slug)] category = Category.search(domain, limit=1) else: category = Category - return category @staticmethod def resolve_categories(self, info, filter, current_page, page_size, search, sort): env = info.context["env"] order = get_search_order(sort) - domain = env['website'].get_current_website().website_domain() - + domain = env["website"].get_current_website().website_domain() if search: for srch in search.split(" "): - domain += [('name', 'ilike', srch)] - - if filter.get('id'): - domain += [('id', 'in', filter['id'])] - + domain += [("name", "ilike", srch)] + if filter.get("ids"): + domain += [("id", "in", filter["ids"])] # Parent if is a Top Category - if filter.get('parent'): - domain += [('parent_id', '=', False)] - - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 - + if filter.get("parent"): + domain += [("parent_id", "=", False)] + offset = get_offset(current_page, page_size) ProductPublicCategory = env["product.public.category"] total_count = ProductPublicCategory.search_count(domain) categories = ProductPublicCategory.search( - domain, limit=page_size, offset=offset, order=order) + domain, limit=page_size, offset=offset, order=order + ) return CategoryList(categories=categories, total_count=total_count) diff --git a/graphql_vuestorefront/schemas/contact_us.py b/vuestorefront/schemas/contact_us.py similarity index 55% rename from graphql_vuestorefront/schemas/contact_us.py rename to vuestorefront/schemas/contact_us.py index bfc3d63..435cf53 100644 --- a/graphql_vuestorefront/schemas/contact_us.py +++ b/vuestorefront/schemas/contact_us.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene -from odoo.addons.graphql_vuestorefront.schemas.objects import Lead +from .objects import Lead class ContactUsParams(graphene.InputObjectType): @@ -24,23 +23,25 @@ class Arguments: @staticmethod def mutate(self, info, contactus): - env = info.context['env'] + env = info.context["env"] data = { - 'contact_name': contactus['name'], - 'email_from': contactus['email'], - 'phone': contactus['phone'], - 'name': contactus['subject'], - 'description': contactus['message'], + "contact_name": contactus["name"], + "email_from": contactus["email"], + "phone": contactus["phone"], + "name": contactus["subject"], + "description": contactus["message"], } # If Contact Us have one Company Name - if contactus.get('company'): - company = {'partner_name': contactus['company']} + if contactus.get("company"): + company = {"partner_name": contactus["company"]} data.update(company) - return env['crm.lead'].sudo().create(data) + return env["crm.lead"].sudo().create(data) class ContactUsMutation(graphene.ObjectType): - contact_us = ContactUs.Field(description='Creates a new lead with the contact information.') + contact_us = ContactUs.Field( + description="Creates a new lead with the contact information." + ) diff --git a/graphql_vuestorefront/schemas/country.py b/vuestorefront/schemas/country.py similarity index 73% rename from graphql_vuestorefront/schemas/country.py rename to vuestorefront/schemas/country.py index 71d1d5c..353710c 100644 --- a/graphql_vuestorefront/schemas/country.py +++ b/vuestorefront/schemas/country.py @@ -1,26 +1,24 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene -from odoo.addons.graphql_vuestorefront.schemas.objects import ( - SortEnum, Country -) +from ..utils import get_offset +from .objects import Country, SortEnum def get_search_order(sort): - sorting = '' + sorting = "" for field, val in sort.items(): if sorting: - sorting += ', ' - sorting += '%s %s' % (field, val.value) + sorting += ", " + sorting += "%s %s" % (field, val.value) # Add id as last factor, so we can consistently get the same results if sorting: - sorting += ', id ASC' + sorting += ", id ASC" else: - sorting = 'id ASC' + sorting = "id ASC" return sorting @@ -55,12 +53,12 @@ class CountryQuery(graphene.ObjectType): current_page=graphene.Int(default_value=1), page_size=graphene.Int(default_value=20), search=graphene.String(default_value=False), - sort=graphene.Argument(CountrySortInput, default_value={}) + sort=graphene.Argument(CountrySortInput, default_value={}), ) @staticmethod def resolve_country(self, info, id): - return info.context['env']['res.country'].search([('id', '=', id)], limit=1) + return info.context["env"]["res.country"].search([("id", "=", id)], limit=1) @staticmethod def resolve_countries(self, info, filter, current_page, page_size, search, sort): @@ -70,17 +68,12 @@ def resolve_countries(self, info, filter, current_page, page_size, search, sort) if search: for srch in search.split(" "): - domain += [('name', 'ilike', srch)] + domain += [("name", "ilike", srch)] - if filter.get('id'): - domain += [('id', '=', filter['id'])] - - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 + if filter.get("id"): + domain += [("id", "=", filter["id"])] + offset = get_offset(current_page, page_size) Country = env["res.country"] total_count = Country.search_count(domain) countries = Country.search(domain, limit=page_size, offset=offset, order=order) diff --git a/graphql_vuestorefront/schemas/invoice.py b/vuestorefront/schemas/invoice.py similarity index 62% rename from graphql_vuestorefront/schemas/invoice.py rename to vuestorefront/schemas/invoice.py index f943a55..3b11241 100644 --- a/graphql_vuestorefront/schemas/invoice.py +++ b/vuestorefront/schemas/invoice.py @@ -1,31 +1,33 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphql import GraphQLError -from odoo.http import request + from odoo import _ +from odoo.http import request -from odoo.addons.graphql_vuestorefront.schemas.objects import ( - SortEnum, Invoice, +from ..utils import get_offset +from .objects import ( + Invoice, + SortEnum, + get_document_count_with_check_access, get_document_with_check_access, - get_document_count_with_check_access ) def get_search_order(sort): - sorting = '' + sorting = "" for field, val in sort.items(): if sorting: - sorting += ', ' - sorting += '%s %s' % (field, val.value) + sorting += ", " + sorting += "%s %s" % (field, val.value) # Add id as last factor, so we can consistently get the same results if sorting: - sorting += ', id ASC' + sorting += ", id ASC" else: - sorting = 'id ASC' + sorting = "id ASC" return sorting @@ -57,14 +59,16 @@ class InvoiceQuery(graphene.ObjectType): Invoices, current_page=graphene.Int(default_value=1), page_size=graphene.Int(default_value=10), - sort=graphene.Argument(InvoiceSortInput, default_value={}) + sort=graphene.Argument(InvoiceSortInput, default_value={}), ) @staticmethod def resolve_invoice(self, info, id): - AccountMove = info.context["env"]['account.move'] - error_msg = 'Invoice does not exist.' - invoice = get_document_with_check_access(AccountMove, [('id', '=', id)], error_msg=error_msg) + AccountMove = info.context["env"]["account.move"] + error_msg = "Invoice does not exist." + invoice = get_document_with_check_access( + AccountMove, [("id", "=", id)], error_msg=error_msg + ) if not invoice: raise GraphQLError(_(error_msg)) return invoice.sudo() @@ -76,17 +80,20 @@ def resolve_invoices(self, info, current_page, page_size, sort): partner = user.partner_id sort_order = get_search_order(sort) domain = [ - ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]) + ("message_partner_ids", "child_of", [partner.commercial_partner_id.id]) ] - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 - + offset = get_offset(current_page, page_size) AccountMove = env["account.move"] - invoices = get_document_with_check_access(AccountMove, domain, sort_order, page_size, offset, - error_msg='Invoice does not exist.') + invoices = get_document_with_check_access( + AccountMove, + domain, + sort_order, + page_size, + offset, + error_msg="Invoice does not exist.", + ) total_count = get_document_count_with_check_access(AccountMove, domain) - return InvoiceList(invoices=invoices and invoices.sudo() or invoices, total_count=total_count) + return InvoiceList( + invoices=invoices and invoices.sudo() or invoices, total_count=total_count + ) diff --git a/graphql_vuestorefront/schemas/mailing_list.py b/vuestorefront/schemas/mailing_list.py similarity index 54% rename from graphql_vuestorefront/schemas/mailing_list.py rename to vuestorefront/schemas/mailing_list.py index e8bafe4..a1534f3 100644 --- a/graphql_vuestorefront/schemas/mailing_list.py +++ b/vuestorefront/schemas/mailing_list.py @@ -1,29 +1,34 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphql import GraphQLError -from odoo.http import request + from odoo import _ +from odoo.http import request + from odoo.addons.website_mass_mailing.controllers.main import MassMailController -from odoo.addons.graphql_vuestorefront.schemas.objects import ( - SortEnum, MailingContact, MailingList -) + +from ..utils import get_offset +from .objects import MailingContact, MailingList, SortEnum + + +def predicate_maillist_id(maillist_id): + return lambda mail: mail.list_id.id == maillist_id def get_search_order(sort): - sorting = '' + sorting = "" for field, val in sort.items(): if sorting: - sorting += ', ' - sorting += '%s %s' % (field, val.value) + sorting += ", " + sorting += "%s %s" % (field, val.value) # Add id as last factor, so we can consistently get the same results if sorting: - sorting += ', id ASC' + sorting += ", id ASC" else: - sorting = 'id ASC' + sorting = "id ASC" return sorting @@ -32,6 +37,7 @@ def get_search_order(sort): # Mailing Contacts # # --------------------- # + class MailingContactFilterInput(graphene.InputObjectType): id = graphene.Int() @@ -57,39 +63,41 @@ class MailingContactQuery(graphene.ObjectType): current_page=graphene.Int(default_value=1), page_size=graphene.Int(default_value=20), search=graphene.String(default_value=False), - sort=graphene.Argument(MailingContactSortInput, default_value={}) + sort=graphene.Argument(MailingContactSortInput, default_value={}), ) @staticmethod - def resolve_mailing_contacts(self, info, filter, current_page, page_size, search, sort): - env = info.context['env'] + def resolve_mailing_contacts( + self, info, filter, current_page, page_size, search, sort + ): + env = info.context["env"] order = get_search_order(sort) - domain = [('email', '=', env.user.email)] + domain = [("email", "=", env.user.email)] if search: for srch in search.split(" "): - domain += [('name', 'ilike', srch)] + domain += [("name", "ilike", srch)] - if filter.get('id', False): - domain += [('id', '=', filter['id'])] + if filter.get("id", False): + domain += [("id", "=", filter["id"])] - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 - - MailingContact = env['mailing.contact'].sudo() + offset = get_offset(current_page, page_size) + MailingContact = env["mailing.contact"].sudo() total_count = MailingContact.search_count(domain) - mailing_contacts = MailingContact.search(domain, limit=page_size, offset=offset, order=order) - return MailingContactList(mailing_contacts=mailing_contacts, total_count=total_count) + mailing_contacts = MailingContact.search( + domain, limit=page_size, offset=offset, order=order + ) + return MailingContactList( + mailing_contacts=mailing_contacts, total_count=total_count + ) # --------------------- # # Mailing List # # --------------------- # + class MailingListFilterInput(graphene.InputObjectType): id = graphene.Int() @@ -120,35 +128,38 @@ class MailingListQuery(graphene.ObjectType): current_page=graphene.Int(default_value=1), page_size=graphene.Int(default_value=20), search=graphene.String(default_value=False), - sort=graphene.Argument(MailingListSortInput, default_value={}) + sort=graphene.Argument(MailingListSortInput, default_value={}), ) @staticmethod def resolve_mailing_list(self, info, id): - return info.context['env']['mailing.list'].sudo().search([('id', '=', id), ('is_public', '=', True)], limit=1) + return ( + info.context["env"]["mailing.list"] + .sudo() + .search([("id", "=", id), ("is_public", "=", True)], limit=1) + ) @staticmethod - def resolve_mailing_lists(self, info, filter, current_page, page_size, search, sort): + def resolve_mailing_lists( + self, info, filter, current_page, page_size, search, sort + ): env = info.context["env"] order = get_search_order(sort) - domain = [('is_public', '=', True)] + domain = [("is_public", "=", True)] if search: for srch in search.split(" "): - domain += [('name', 'ilike', srch)] + domain += [("name", "ilike", srch)] - if filter.get('id', False): - domain += [('id', '=', filter['id'])] - - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 + if filter.get("id", False): + domain += [("id", "=", filter["id"])] + offset = get_offset(current_page, page_size) MailingList = env["mailing.list"].sudo() total_count = MailingList.search_count(domain) - mailing_lists = MailingList.search(domain, limit=page_size, offset=offset, order=order) + mailing_lists = MailingList.search( + domain, limit=page_size, offset=offset, order=order + ) return MailingListList(mailing_lists=mailing_lists, total_count=total_count) @@ -160,11 +171,13 @@ class Arguments: @staticmethod def mutate(self, info, email): - env = info.context['env'] - website = env['website'].get_current_website() + env = info.context["env"] + website = env["website"].get_current_website() if website.vsf_mailing_list_id: - MassMailController().subscribe(website.vsf_mailing_list_id.id, email, 'email') + MassMailController().subscribe( + website.vsf_mailing_list_id.id, email, "email" + ) return NewsletterSubscribe(subscribed=True) return NewsletterSubscribe(subscribed=False) @@ -183,7 +196,7 @@ class Arguments: @staticmethod def mutate(self, info, mailings): - env = info.context['env'] + env = info.context["env"] user = request.env.user # Company name @@ -200,43 +213,69 @@ def mutate(self, info, mailings): else: country_id = False - mailing_contact = env['mailing.contact'].sudo().search([('email', '=', user.email)], limit=1) + mailing_contact = ( + env["mailing.contact"].sudo().search([("email", "=", user.email)], limit=1) + ) for mailing in mailings: - maillist_id = mailing['mailinglistId'] - optout = mailing['optout'] + maillist_id = mailing["mailinglistId"] + optout = mailing["optout"] - mailing_list = env['mailing.list'].sudo().search([('id', '=', maillist_id)], limit=1) + mailing_list = ( + env["mailing.list"].sudo().search([("id", "=", maillist_id)], limit=1) + ) if not mailing_list: - raise GraphQLError(_('Maillist does not exist.')) + raise GraphQLError(_("Maillist does not exist.")) if mailing_contact: - line = mailing_contact.subscription_list_ids.filtered(lambda mail: mail.list_id.id == maillist_id) + line = mailing_contact.subscription_list_ids.filtered( + predicate_maillist_id(maillist_id) + ) - if not mailing_contact.company_name or (company_name and mailing_contact.company_name != company_name): - mailing_contact.update({'company_name': company_name}) + if not mailing_contact.company_name or ( + company_name and mailing_contact.company_name != company_name + ): + mailing_contact.update({"company_name": company_name}) - if not mailing_contact.country_id or (country_id and mailing_contact.country_id != country_id): - mailing_contact.update({'country_id': country_id}) + if not mailing_contact.country_id or ( + country_id and mailing_contact.country_id != country_id + ): + mailing_contact.update({"country_id": country_id}) if line: - line.update({'opt_out': optout}) + line.update({"opt_out": optout}) else: mailing_contact.write( - {'subscription_list_ids': [(0, 0, {'list_id': mailing_list.id, 'opt_out': optout})], }) + { + "subscription_list_ids": [ + (0, 0, {"list_id": mailing_list.id, "opt_out": optout}) + ], + } + ) else: - mailing_contact = env['mailing.contact'].sudo().create({ - 'name': user.name, - 'country_id': country_id, - 'email': user.email, - 'company_name': company_name, - 'subscription_list_ids': [(0, 0, {'list_id': mailing_list.id, 'opt_out': optout})], - }) + mailing_contact = ( + env["mailing.contact"] + .sudo() + .create( + { + "name": user.name, + "country_id": country_id, + "email": user.email, + "company_name": company_name, + "subscription_list_ids": [ + (0, 0, {"list_id": mailing_list.id, "opt_out": optout}) + ], + } + ) + ) return mailing_contact class NewsletterSubscribeMutation(graphene.ObjectType): - newsletter_subscribe = NewsletterSubscribe.Field(description='Subscribe to newsletter.') + newsletter_subscribe = NewsletterSubscribe.Field( + description="Subscribe to newsletter." + ) user_add_multiple_mailing = UserAddMultipleMailing.Field( - description='Create or Update Multiple Mailing Contact information') + description="Create or Update Multiple Mailing Contact information" + ) diff --git a/graphql_vuestorefront/schemas/objects.py b/vuestorefront/schemas/objects.py similarity index 78% rename from graphql_vuestorefront/schemas/objects.py rename to vuestorefront/schemas/objects.py index 97a515a..847cc61 100644 --- a/graphql_vuestorefront/schemas/objects.py +++ b/vuestorefront/schemas/objects.py @@ -1,65 +1,114 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphene.types import generic from graphql import GraphQLError -from odoo import SUPERUSER_ID, _ -from odoo.addons.http_routing.models.ir_http import slugify -from odoo.addons.graphql_base import OdooObjectType +from odoo import SUPERUSER_ID, _ from odoo.exceptions import AccessError from odoo.http import request +from odoo.addons.graphql_base import OdooObjectType +from odoo.addons.http_routing.models.ir_http import slugify # --------------------- # # ENUMS # # --------------------- # -AddressType = graphene.Enum('AddressType', [('Contact', 'contact'), ('InvoiceAddress', 'invoice'), - ('DeliveryAddress', 'delivery'), ('OtherAddress', 'other'), - ('PrivateAddress', 'private')]) - -VariantCreateMode = graphene.Enum('VariantCreateMode', [('Instantly', 'always'), ('Dynamically', 'dynamically'), - ('NeverOption', 'no_variant')]) - -FilterVisibility = graphene.Enum('FilterVisibility', [('Visible', 'visible'), ('Hidden', 'hidden')]) - -OrderStage = graphene.Enum('OrderStage', [('Quotation', 'draft'), ('QuotationSent', 'sent'), - ('SalesOrder', 'sale'), ('Locked', 'done'), ('Cancelled', 'cancel')]) - -InvoiceStatus = graphene.Enum('InvoiceStatus', [('UpsellingOpportunity', 'upselling'), ('FullyInvoiced', 'invoiced'), - ('ToInvoice', 'to invoice'), ('NothingtoInvoice', 'no')]) - -InvoiceState = graphene.Enum('InvoiceState', [('Draft', 'draft'), ('Posted', 'posted'), ('Cancelled', 'cancel')]) - -PaymentTransactionState = graphene.Enum('PaymentTransactionState', [('Draft', 'draft'), ('Pending', 'pending'), - ('Authorized', 'authorized'), ('Confirmed', 'done'), - ('Canceled', 'cancel'), ('Error', 'error')]) +AddressType = graphene.Enum( + "AddressType", + [ + ("Contact", "contact"), + ("InvoiceAddress", "invoice"), + ("DeliveryAddress", "delivery"), + ("OtherAddress", "other"), + ("PrivateAddress", "private"), + ], +) + +VariantCreateMode = graphene.Enum( + "VariantCreateMode", + [ + ("Instantly", "always"), + ("Dynamically", "dynamically"), + ("NeverOption", "no_variant"), + ], +) + +FilterVisibility = graphene.Enum( + "FilterVisibility", [("Visible", "visible"), ("Hidden", "hidden")] +) + +OrderStage = graphene.Enum( + "OrderStage", + [ + ("Quotation", "draft"), + ("QuotationSent", "sent"), + ("SalesOrder", "sale"), + ("Locked", "done"), + ("Cancelled", "cancel"), + ], +) + +InvoiceStatus = graphene.Enum( + "InvoiceStatus", + [ + ("UpsellingOpportunity", "upselling"), + ("FullyInvoiced", "invoiced"), + ("ToInvoice", "to invoice"), + ("NothingtoInvoice", "no"), + ], +) + +InvoiceState = graphene.Enum( + "InvoiceState", [("Draft", "draft"), ("Posted", "posted"), ("Cancelled", "cancel")] +) + +PaymentTransactionState = graphene.Enum( + "PaymentTransactionState", + [ + ("Draft", "draft"), + ("Pending", "pending"), + ("Authorized", "authorized"), + ("Confirmed", "done"), + ("Canceled", "cancel"), + ("Error", "error"), + ], +) class SortEnum(graphene.Enum): - ASC = 'ASC' - DESC = 'DESC' + ASC = "ASC" + DESC = "DESC" # --------------------- # # Functions # # --------------------- # -def get_document_with_check_access(model, domain=[], order=None, limit=20, offset=0, access_token=None, - error_msg='This document does not exist.'): + +def get_document_with_check_access( + model, + domain=None, + order=None, + limit=20, + offset=0, + access_token=None, + error_msg="This document does not exist.", +): + if domain is None: + domain = [] if access_token: model = model.sudo() - domain = [('access_token', '=', access_token)] + domain = [("access_token", "=", access_token)] document = model.search(domain, order=order, limit=limit, offset=offset) document_sudo = document.with_user(SUPERUSER_ID).exists() if document and not document_sudo: raise GraphQLError(_(error_msg)) try: - document.check_access_rights('read') - document.check_access_rule('read') + document.check_access_rights("read") + document.check_access_rule("read") except AccessError: return [] return document_sudo @@ -67,21 +116,23 @@ def get_document_with_check_access(model, domain=[], order=None, limit=20, offse def get_document_count_with_check_access(model, domain): try: - model.check_access_rights('read') - model.check_access_rule('read') + model.check_access_rights("read") + model.check_access_rule("read") except AccessError: return 0 return model.search_count(domain) def get_product_pricing_info(env, product): - website = env['website'].get_current_website() + website = env["website"].get_current_website() pricelist = website.get_current_pricelist() - return product and product._get_combination_info_variant(pricelist=pricelist) or None + return ( + product and product._get_combination_info_variant(pricelist=pricelist) or None + ) def product_is_in_wishlist(env, product): - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website return product._is_in_wishlist() @@ -90,6 +141,7 @@ def product_is_in_wishlist(env, product): # Objects # # --------------------- # + class Lead(OdooObjectType): id = graphene.Int(required=True) name = graphene.String() @@ -160,7 +212,7 @@ def resolve_state(self, info): return self.state_id or None def resolve_image(self, info): - return '/web/image/res.company/{}/image_1920'.format(self.id) + return f"/web/image/res.company/{self.id}/image_1920" class Pricelist(OdooObjectType): @@ -188,6 +240,7 @@ class Partner(OdooObjectType): billing_address = graphene.Field(lambda: Partner) is_company = graphene.Boolean(required=True) company = graphene.Field(lambda: Partner) + company_name = graphene.String() contacts = graphene.List(graphene.NonNull(lambda: Partner)) signup_token = graphene.String() signup_valid = graphene.String() @@ -207,7 +260,9 @@ def resolve_address_type(self, info): return self.type or None def resolve_billing_address(self, info): - billing_address = self.child_ids.filtered(lambda a: a.type and a.type == 'invoice') + billing_address = self.child_ids.filtered( + lambda a: a.type and a.type == "invoice" + ) return billing_address and billing_address[0] or None def resolve_company(self, info): @@ -220,15 +275,18 @@ def resolve_parent_id(self, info): return self.parent_id or None def resolve_image(self, info): - return '/web/image/res.partner/{}/image_1920'.format(self.id) + return f"/web/image/res.partner/{self.id}/image_1920" def resolve_public_pricelist(self, info): - website = self.env['website'].get_current_website() + website = self.env["website"].get_current_website() partner = website.user_id.sudo().partner_id - return partner.last_website_so_id.pricelist_id or partner.property_product_pricelist + return ( + partner.last_website_so_id.pricelist_id + or partner.property_product_pricelist + ) def resolve_current_pricelist(self, info): - website = self.env['website'].get_current_website() + website = self.env["website"].get_current_website() return website.get_current_pricelist() @@ -267,7 +325,7 @@ def resolve_childs(self, info): return self.child_id or None def resolve_slug(self, info): - return self.website_slug + return self.slug def resolve_products(self, info): return self.product_tmpl_ids or None @@ -282,7 +340,9 @@ class AttributeValue(OdooObjectType): display_type = graphene.String() html_color = graphene.String() search = graphene.String() - price_extra = graphene.Float(description='Not use in the return Attributes List of the Products Query') + price_extra = graphene.Float( + description="Not use in the return Attributes List of the Products Query" + ) attribute = graphene.Field(lambda: Attribute) def resolve_id(self, info): @@ -291,7 +351,7 @@ def resolve_id(self, info): def resolve_search(self, info): attribute_id = self.attribute_id.id attribute_value_id = self.id - return '{}-{}'.format(attribute_id, attribute_value_id) or None + return f"{attribute_id}-{attribute_value_id}" or None def resolve_attribute(self, info): return self.attribute_id or None @@ -326,7 +386,7 @@ def resolve_id(self, info): return self.id or None def resolve_image(self, info): - return '/web/image/product.image/{}/image_1920'.format(self.id) + return f"/web/image/product.image/{self.id}/image_1920" def resolve_image_filename(self, info): return slugify(self.name) @@ -376,28 +436,44 @@ class Product(OdooObjectType): alternative_products = graphene.List(graphene.NonNull(lambda: Product)) accessory_products = graphene.List(graphene.NonNull(lambda: Product)) # Specific to use in Product Variant - combination_info_variant = generic.GenericScalar(description='Specific to Product Variant') - variant_price = graphene.Float(description='Specific to Product Variant') - variant_price_after_discount = graphene.Float(description='Specific to Product Variant') - variant_has_discounted_price = graphene.Boolean(description='Specific to Product Variant') - is_variant_possible = graphene.Boolean(description='Specific to Product Variant') - variant_attribute_values = graphene.List(graphene.NonNull(lambda: AttributeValue), - description='Specific to Product Variant') - product_template = graphene.Field((lambda: Product), description='Specific to Product Variant') + combination_info_variant = generic.GenericScalar( + description="Specific to Product Variant" + ) + variant_price = graphene.Float(description="Specific to Product Variant") + variant_price_after_discount = graphene.Float( + description="Specific to Product Variant" + ) + variant_has_discounted_price = graphene.Boolean( + description="Specific to Product Variant" + ) + is_variant_possible = graphene.Boolean(description="Specific to Product Variant") + variant_attribute_values = graphene.List( + graphene.NonNull(lambda: AttributeValue), + description="Specific to Product Variant", + ) + product_template = graphene.Field( + (lambda: Product), description="Specific to Product Variant" + ) # Specific to use in Product Template - combination_info = generic.GenericScalar(description='Specific to Product Template') - price = graphene.Float(description='Specific to Product Template') - attribute_values = graphene.List(graphene.NonNull(lambda: AttributeValue), - description='Specific to Product Template') - product_variants = graphene.List(graphene.NonNull(lambda: Product), description='Specific to Product Template') - first_variant = graphene.Field((lambda: Product), description='Specific to use in Product Template') + combination_info = generic.GenericScalar(description="Specific to Product Template") + price = graphene.Float(description="Specific to Product Template") + attribute_values = graphene.List( + graphene.NonNull(lambda: AttributeValue), + description="Specific to Product Template", + ) + product_variants = graphene.List( + graphene.NonNull(lambda: Product), description="Specific to Product Template" + ) + first_variant = graphene.Field( + (lambda: Product), description="Specific to use in Product Template" + ) json_ld = generic.GenericScalar() def resolve_type_id(self, info): - if self.detailed_type == 'product': - return 'simple' + if self.detailed_type == "product": + return "simple" else: - return 'configurable' + return "configurable" def resolve_visibility(self, info): if self.website_published: @@ -430,22 +506,28 @@ def resolve_meta_description(self, info): return self.website_meta_description or None def resolve_image(self, info): - return '/web/image/{}/{}/image_1920'.format(self._name, self.id) + return f"/web/image/{self._name}/{self.id}/image_1920" def resolve_small_image(self, info): - return '/web/image/{}/{}/image_128'.format(self._name, self.id) + return f"/web/image/{self._name}/{self.id}/image_128" def resolve_image_filename(self, info): return slugify(self.name) def resolve_thumbnail(self, info): - return '/web/image/{}/{}/image_512'.format(self._name, self.id) + return f"/web/image/{self._name}/{self.id}/image_512" def resolve_categories(self, info): - website = self.env['website'].get_current_website() + website = self.env["website"].get_current_website() if website: - return self.public_categ_ids.filtered( - lambda c: not c.website_id or c.website_id and c.website_id.id == website.id) or None + return ( + self.public_categ_ids.filtered( + lambda c: not c.website_id + or c.website_id + and c.website_id.id == website.id + ) + or None + ) return self.public_categ_ids or None def resolve_allow_out_of_stock(self, info): @@ -466,16 +548,18 @@ def resolve_is_in_wishlist(self, info): return bool(is_in_wishlist) def resolve_media_gallery(self, info): - if self._name == 'product.template': + if self._name == "product.template": return self.product_template_image_ids or None else: - return self.product_template_image_ids + self.product_variant_image_ids or None + return ( + self.product_template_image_ids + self.product_variant_image_ids or None + ) def resolve_qty(self, info): return self.free_qty def resolve_slug(self, info): - return self.website_slug + return self.slug def resolve_alternative_products(self, info): return self.alternative_product_ids or None @@ -492,17 +576,17 @@ def resolve_combination_info_variant(self, info): def resolve_variant_price(self, info): env = info.context["env"] pricing_info = get_product_pricing_info(env, self) - return pricing_info['list_price'] or None + return pricing_info["list_price"] or None def resolve_variant_price_after_discount(self, info): env = info.context["env"] pricing_info = get_product_pricing_info(env, self) - return pricing_info['price'] or None + return pricing_info["price"] or None def resolve_variant_has_discounted_price(self, info): env = info.context["env"] pricing_info = get_product_pricing_info(env, self) - return pricing_info['has_discounted_price'] + return pricing_info["has_discounted_price"] def resolve_is_variant_possible(self, info): return self._is_variant_possible() @@ -591,13 +675,21 @@ def resolve_quantity(self, info): def resolve_gift_card(self, info): gift_card = None - if self.coupon_id and self.coupon_id.program_type and self.coupon_id.program_type == 'gift_card': + if ( + self.coupon_id + and self.coupon_id.program_type + and self.coupon_id.program_type == "gift_card" + ): gift_card = self.coupon_id return gift_card def resolve_coupon(self, info): coupon = None - if self.coupon_id and self.coupon_id.program_type and self.coupon_id.program_type == 'coupons': + if ( + self.coupon_id + and self.coupon_id.program_type + and self.coupon_id.program_type == "coupons" + ): coupon = self.coupon_id return coupon @@ -619,10 +711,12 @@ class ShippingMethod(OdooObjectType): product = graphene.Field(lambda: Product) def resolve_price(self, info): - website = self.env['website'].get_current_website() + website = self.env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=True) - return self.rate_shipment(order)['price'] if self.free_over else self.fixed_price + return ( + self.rate_shipment(order)["price"] if self.free_over else self.fixed_price + ) def resolve_product(self, info): return self.product_id or None @@ -697,26 +791,38 @@ def resolve_transactions(self, info): def resolve_last_transaction(self, info): if self.transaction_ids: - return self.transaction_ids.sorted(key=lambda r: r.create_date, reverse=True)[0] + return self.transaction_ids.sorted( + key=lambda r: r.create_date, reverse=True + )[0] return None def resolve_amount_subtotal(self, info): - subtotal_lines = self.order_line.filtered(lambda l: not l.is_reward_line) - return sum(subtotal_lines.mapped('price_total')) - self.amount_delivery + subtotal_lines = self.order_line.filtered(lambda r: not r.is_reward_line) + return sum(subtotal_lines.mapped("price_total")) - self.amount_delivery def resolve_amount_discounts(self, info): return self.reward_amount def resolve_amount_gift_cards(self, info): - return sum(self.order_line.filtered( - lambda l: l.coupon_id and l.coupon_id.program_type and - l.coupon_id.program_type == 'gift_card').mapped('price_total')) + return sum( + self.order_line.filtered( + lambda r: r.coupon_id + and r.coupon_id.program_type + and r.coupon_id.program_type == "gift_card" + ).mapped("price_total") + ) def resolve_coupons(self, info): - return self.applied_coupon_ids.filtered(lambda c: c.program_type == 'coupons') or None + return ( + self.applied_coupon_ids.filtered(lambda c: c.program_type == "coupons") + or None + ) def resolve_gift_cards(self, info): - return self.applied_coupon_ids.filtered(lambda c: c.program_type == 'gift_card') or None + return ( + self.applied_coupon_ids.filtered(lambda c: c.program_type == "gift_card") + or None + ) def resolve_cart_quantity(self, info): return self.cart_quantity or None @@ -802,7 +908,7 @@ class PaymentIcon(OdooObjectType): image = graphene.String() def resolve_image(self, info): - return '/web/image/payment.icon/{}/image'.format(self.id) + return f"/web/image/payment.icon/{self.id}/image" class PaymentProvider(OdooObjectType): @@ -835,7 +941,9 @@ class MailingContact(OdooObjectType): name = graphene.String() email = graphene.String() company_name = graphene.String() - subscription_list = graphene.List(graphene.NonNull(lambda: MailingContactSubscription)) + subscription_list = graphene.List( + graphene.NonNull(lambda: MailingContactSubscription) + ) def resolve_country(self, info): return self.country_id or None @@ -893,4 +1001,4 @@ class WebsiteMenuImage(OdooObjectType): button_url = graphene.String() def resolve_image(self, info): - return '/web/image/website.menu.image/{}/image'.format(self.id) + return f"/web/image/website.menu.image/{self.id}/image" diff --git a/graphql_vuestorefront/schemas/order.py b/vuestorefront/schemas/order.py similarity index 52% rename from graphql_vuestorefront/schemas/order.py rename to vuestorefront/schemas/order.py index 99e5880..6fefa81 100644 --- a/graphql_vuestorefront/schemas/order.py +++ b/vuestorefront/schemas/order.py @@ -1,35 +1,44 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphql import GraphQLError -from odoo.http import request + from odoo import _ +from odoo.http import request -from odoo.addons.graphql_vuestorefront.schemas.objects import ( - SortEnum, OrderStage, InvoiceStatus, Order, ShippingMethod, +from ..utils import get_offset +from .objects import ( + InvoiceStatus, + Order, + OrderStage, + ShippingMethod, + SortEnum, + get_document_count_with_check_access, get_document_with_check_access, - get_document_count_with_check_access ) def get_search_order(sort): - sorting = '' + sorting = "" for field, val in sort.items(): if sorting: - sorting += ', ' - sorting += '%s %s' % (field, val.value) + sorting += ", " + sorting += "%s %s" % (field, val.value) # Add id as last factor, so we can consistently get the same results if sorting: - sorting += ', id ASC' + sorting += ", id ASC" else: - sorting = 'id ASC' + sorting = "id ASC" return sorting +class UpdateOrderInput(graphene.InputObjectType): + client_order_ref = graphene.String() + + class OrderFilterInput(graphene.InputObjectType): stages = graphene.List(OrderStage) invoice_status = graphene.List(InvoiceStatus) @@ -63,17 +72,17 @@ class OrderQuery(graphene.ObjectType): filter=graphene.Argument(OrderFilterInput, default_value={}), current_page=graphene.Int(default_value=1), page_size=graphene.Int(default_value=10), - sort=graphene.Argument(OrderSortInput, default_value={}) - ) - delivery_methods = graphene.List( - graphene.NonNull(ShippingMethod) + sort=graphene.Argument(OrderSortInput, default_value={}), ) + delivery_methods = graphene.List(graphene.NonNull(ShippingMethod)) @staticmethod def resolve_order(self, info, id): - SaleOrder = info.context['env']['sale.order'] - error_msg = 'Sale Order does not exist.' - order = get_document_with_check_access(SaleOrder, [('id', '=', id)], error_msg=error_msg) + SaleOrder = info.context["env"]["sale.order"] + error_msg = "Sale Order does not exist." + order = get_document_with_check_access( + SaleOrder, [("id", "=", id)], error_msg=error_msg + ) if not order: raise GraphQLError(_(error_msg)) return order.sudo() @@ -85,43 +94,66 @@ def resolve_orders(self, info, filter, current_page, page_size, sort): partner = user.partner_id sort_order = get_search_order(sort) domain = [ - ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), + ("message_partner_ids", "child_of", [partner.commercial_partner_id.id]), ] # Filter by stages or default to sales and done - if filter.get('stages', False): - stages = [stage.value for stage in filter['stages']] - domain += [('state', 'in', stages)] + if filter.get("stages", False): + stages = [stage.value for stage in filter["stages"]] + domain += [("state", "in", stages)] else: - domain += [('state', 'in', ['sale', 'done'])] + domain += [("state", "in", ["sale", "done"])] # Filter by invoice status - if filter.get('invoice_status', False): - invoice_status = [invoice_status.value for invoice_status in filter['invoice_status']] - domain += [('invoice_status', 'in', invoice_status)] - - # First offset is 0 but first page is 1 - if current_page > 1: - offset = (current_page - 1) * page_size - else: - offset = 0 + if filter.get("invoice_status", False): + invoice_status = [ + invoice_status.value for invoice_status in filter["invoice_status"] + ] + domain += [("invoice_status", "in", invoice_status)] + offset = get_offset(current_page, page_size) SaleOrder = env["sale.order"] - orders = get_document_with_check_access(SaleOrder, domain, sort_order, page_size, offset, - error_msg='Sale Order does not exist.') + orders = get_document_with_check_access( + SaleOrder, + domain, + sort_order, + page_size, + offset, + error_msg="Sale Order does not exist.", + ) total_count = get_document_count_with_check_access(SaleOrder, domain) - return OrderList(orders=orders and orders.sudo() or orders, total_count=total_count) + return OrderList( + orders=orders and orders.sudo() or orders, total_count=total_count + ) @staticmethod def resolve_delivery_methods(self, info): - """ Get all shipping/delivery methods """ - env = info.context['env'] - website = env['website'].get_current_website() + """Get all shipping/delivery methods.""" + env = info.context["env"] + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() return order._get_delivery_methods() +class UpdateOrder(graphene.Mutation): + class Arguments: + data = UpdateOrderInput(required=True) + + Output = Order + + @staticmethod + def mutate(self, info, data): + env = info.context["env"] + website = env["website"].get_current_website() + request.website = website + order = website.sale_get_order(force_create=True) + vals = order.prepare_vsf_vals(data) + if vals: + order.write(vals) + return order + + class ApplyCoupon(graphene.Mutation): class Arguments: promo = graphene.String() @@ -131,20 +163,22 @@ class Arguments: @staticmethod def mutate(self, info, promo): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) coupon_status = order._try_apply_code(promo) - if 'error' in coupon_status: - raise GraphQLError(coupon_status['error']) + if "error" in coupon_status: + raise GraphQLError(coupon_status["error"]) # Apply Coupon order._update_programs_and_rewards() order._auto_apply_rewards() order.action_open_reward_wizard() - return ApplyCoupon(error=coupon_status.get('error') or coupon_status.get('not_found')) + return ApplyCoupon( + error=coupon_status.get("error") or coupon_status.get("not_found") + ) class ApplyGiftCard(graphene.Mutation): @@ -156,22 +190,25 @@ class Arguments: @staticmethod def mutate(self, info, promo): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) gift_card_status = order._try_apply_code(promo) - if 'error' in gift_card_status: - raise GraphQLError(gift_card_status['error']) + if "error" in gift_card_status: + raise GraphQLError(gift_card_status["error"]) # Apply Coupon order._update_programs_and_rewards() order._auto_apply_rewards() order.action_open_reward_wizard() - return ApplyGiftCard(error=gift_card_status.get('error') or gift_card_status.get('not_found')) + return ApplyGiftCard( + error=gift_card_status.get("error") or gift_card_status.get("not_found") + ) class OrderMutation(graphene.ObjectType): - apply_coupon = ApplyCoupon.Field(description='Apply Coupon') - apply_gift_card = ApplyGiftCard.Field(description='Apply Gift Card') + update_order = UpdateOrder.Field(description="Update Order") + apply_coupon = ApplyCoupon.Field(description="Apply Coupon") + apply_gift_card = ApplyGiftCard.Field(description="Apply Gift Card") diff --git a/graphql_vuestorefront/schemas/payment.py b/vuestorefront/schemas/payment.py similarity index 60% rename from graphql_vuestorefront/schemas/payment.py rename to vuestorefront/schemas/payment.py index d236c4a..e5a9526 100644 --- a/graphql_vuestorefront/schemas/payment.py +++ b/vuestorefront/schemas/payment.py @@ -1,21 +1,22 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphene.types import generic from graphql import GraphQLError + from odoo import _ from odoo.http import request from odoo.osv import expression from odoo.addons.payment import utils as payment_utils -from odoo.addons.payment_adyen_vsf.const import CURRENCY_DECIMALS -from odoo.addons.graphql_vuestorefront.schemas.objects import PaymentProvider, PaymentTransaction -from odoo.addons.graphql_vuestorefront.schemas.shop import Cart, CartData -from odoo.addons.website_sale.controllers.main import PaymentPortal from odoo.addons.payment_adyen.controllers.main import AdyenController +from odoo.addons.payment_adyen_vsf.const import CURRENCY_DECIMALS from odoo.addons.payment_adyen_vsf.controllers.main import AdyenControllerInherit +from odoo.addons.website_sale.controllers.main import PaymentPortal + +from .objects import PaymentProvider, PaymentTransaction +from .shop import Cart, CartData class PaymentQuery(graphene.ObjectType): @@ -31,7 +32,7 @@ class PaymentQuery(graphene.ObjectType): PaymentTransaction, required=True, id=graphene.Int(default_value=None), - reference=graphene.String(default_value=None) + reference=graphene.String(default_value=None), ) payment_confirmation = graphene.Field( Cart, @@ -40,71 +41,87 @@ class PaymentQuery(graphene.ObjectType): @staticmethod def resolve_payment_provider(self, info, id): env = info.context["env"] - PaymentProvider = env['payment.provider'].sudo() - website = env['website'].get_current_website() + PaymentProvider = env["payment.provider"].sudo() + website = env["website"].get_current_website() request.website = website domain = [ - ('id', '=', id), - ('state', 'in', ['enabled', 'test']), + ("id", "=", id), + ("state", "in", ["enabled", "test"]), ] payment_provider = PaymentProvider.search(domain, limit=1) if not payment_provider: - raise GraphQLError(_('Payment provider does not exist.')) + raise GraphQLError(_("Payment provider does not exist.")) return payment_provider @staticmethod def resolve_payment_providers(self, info): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() - domain = expression.AND([ - ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', order.company_id.id)], - ['|', ('website_id', '=', False), ('website_id', '=', website.id)], - ['|', ('available_country_ids', '=', False), ('available_country_ids', 'in', [order.partner_id.country_id.id])] - ]) - return env['payment.provider'].sudo().search(domain) + domain = expression.AND( + [ + [ + "&", + ("state", "in", ["enabled", "test"]), + ("company_id", "=", order.company_id.id), + ], + ["|", ("website_id", "=", False), ("website_id", "=", website.id)], + [ + "|", + ("available_country_ids", "=", False), + ("available_country_ids", "in", [order.partner_id.country_id.id]), + ], + ] + ) + return env["payment.provider"].sudo().search(domain) @staticmethod def resolve_payment_transaction(self, info, id, reference): env = info.context["env"] - PaymentTransaction = env['payment.transaction'] + PaymentTransaction = env["payment.transaction"] if id: - payment_transaction = PaymentTransaction.sudo().search([('id', '=', id)], limit=1) + payment_transaction = PaymentTransaction.sudo().search( + [("id", "=", id)], limit=1 + ) elif reference: - payment_transaction = PaymentTransaction.sudo().search([('reference', '=', reference)], limit=1) + payment_transaction = PaymentTransaction.sudo().search( + [("reference", "=", reference)], limit=1 + ) else: payment_transaction = None if not payment_transaction: - raise GraphQLError(_('Payment Transaction does not exist.')) + raise GraphQLError(_("Payment Transaction does not exist.")) return payment_transaction @staticmethod def resolve_payment_confirmation(self, info): env = info.context["env"] - PaymentTransaction = env['payment.transaction'] - Order = env['sale.order'] + PaymentTransaction = env["payment.transaction"] + Order = env["sale.order"] # Pass in the session the sale_order created in vsf - payment_transaction_id = request.session.get('__payment_monitored_tx_ids__') + payment_transaction_id = request.session.get("__payment_monitored_tx_ids__") if payment_transaction_id and payment_transaction_id[0]: - payment_transaction = PaymentTransaction.sudo().search([('id', '=', payment_transaction_id[0])], limit=1) + payment_transaction = PaymentTransaction.sudo().search( + [("id", "=", payment_transaction_id[0])], limit=1 + ) sale_order_id = payment_transaction.sale_order_ids.ids[0] if sale_order_id: - order = Order.sudo().search([('id', '=', sale_order_id)], limit=1) + order = Order.sudo().search([("id", "=", sale_order_id)], limit=1) if order.exists(): return CartData(order=order) - raise GraphQLError(_('Cart does not exist')) + raise GraphQLError(_("Cart does not exist")) class MakeGiftCardPayment(graphene.Mutation): @@ -113,7 +130,7 @@ class MakeGiftCardPayment(graphene.Mutation): @staticmethod def mutate(self, info): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() tx = order.get_portal_last_transaction() @@ -126,13 +143,16 @@ def mutate(self, info): class PaymentMutation(graphene.ObjectType): - make_gift_card_payment = MakeGiftCardPayment.Field(description='Pay the order only with gift card.') + make_gift_card_payment = MakeGiftCardPayment.Field( + description="Pay the order only with gift card." + ) # -------------------------------- # # Adyen Payment # # -------------------------------- # + class AdyenProviderInfoResult(graphene.ObjectType): adyen_provider_info = generic.GenericScalar() @@ -162,19 +182,19 @@ class Arguments: @staticmethod def mutate(self, info, provider_id): env = info.context["env"] - PaymentProvider = env['payment.provider'].sudo() - website = env['website'].get_current_website() + PaymentProvider = env["payment.provider"].sudo() + website = env["website"].get_current_website() request.website = website domain = [ - ('id', '=', provider_id), - ('state', 'in', ['enabled', 'test']), + ("id", "=", provider_id), + ("state", "in", ["enabled", "test"]), ] payment_provider_id = PaymentProvider.search(domain, limit=1) if not payment_provider_id: - raise GraphQLError(_('Payment Provider does not exist.')) + raise GraphQLError(_("Payment Provider does not exist.")) - if not payment_provider_id.code == 'adyen': + if not payment_provider_id.code == "adyen": raise GraphQLError(_('Payment Provider "Adyen" does not exist.')) adyen_provider_info = AdyenController().adyen_provider_info( @@ -193,27 +213,27 @@ class Arguments: @staticmethod def mutate(self, info, provider_id): env = info.context["env"] - PaymentProvider = env['payment.provider'].sudo() - website = env['website'].get_current_website() + PaymentProvider = env["payment.provider"].sudo() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() domain = [ - ('id', '=', provider_id), - ('state', 'in', ['enabled', 'test']), + ("id", "=", provider_id), + ("state", "in", ["enabled", "test"]), ] payment_provider_id = PaymentProvider.search(domain, limit=1) if not payment_provider_id: - raise GraphQLError(_('Payment Provider does not exist.')) + raise GraphQLError(_("Payment Provider does not exist.")) - if not payment_provider_id.code == 'adyen': + if not payment_provider_id.code == "adyen": raise GraphQLError(_('Payment Provider "Adyen" does not exist.')) adyen_payment_methods = AdyenController().adyen_payment_methods( provider_id=payment_provider_id.id, amount=order.amount_total, currency_id=order.currency_id.id, - partner_id=order.partner_id.id + partner_id=order.partner_id.id, ) return AdyenPaymentMethodsResult(adyen_payment_methods=adyen_payment_methods) @@ -228,21 +248,21 @@ class Arguments: @staticmethod def mutate(self, info, provider_id): env = info.context["env"] - PaymentProvider = env['payment.provider'].sudo() - PaymentTransaction = env['payment.transaction'].sudo() - website = env['website'].get_current_website() + PaymentProvider = env["payment.provider"].sudo() + PaymentTransaction = env["payment.transaction"].sudo() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order() domain = [ - ('id', '=', provider_id), - ('state', 'in', ['enabled', 'test']), + ("id", "=", provider_id), + ("state", "in", ["enabled", "test"]), ] payment_provider_id = PaymentProvider.search(domain, limit=1) if not payment_provider_id: - raise GraphQLError(_('Payment Provider does not exist.')) + raise GraphQLError(_("Payment Provider does not exist.")) - if not payment_provider_id.code == 'adyen': + if not payment_provider_id.code == "adyen": raise GraphQLError(_('Payment Provider "Adyen" does not exist.')) transaction = PaymentPortal().shop_payment_transaction( @@ -252,12 +272,14 @@ def mutate(self, info, provider_id): amount=order.amount_total, currency_id=order.currency_id.id, partner_id=order.partner_id.id, - flow='direct', + flow="direct", tokenization_requested=False, - landing_route='/shop/payment/validate' + landing_route="/shop/payment/validate", ) - transaction_id = PaymentTransaction.search([('reference', '=', transaction['reference'])], limit=1) + transaction_id = PaymentTransaction.search( + [("reference", "=", transaction["reference"])], limit=1 + ) # Update the field created_on_vsf transaction_id.created_on_vsf = True @@ -270,38 +292,54 @@ class Arguments: provider_id = graphene.Int(required=True) transaction_reference = graphene.String(required=True) access_token = graphene.String(required=True) - payment_method = generic.GenericScalar(required=True, description='Return state.data.paymentMethod') - browser_info = generic.GenericScalar(required=True, description='Return state.data.browserInfo') + payment_method = generic.GenericScalar( + required=True, description="Return state.data.paymentMethod" + ) + browser_info = generic.GenericScalar( + required=True, description="Return state.data.browserInfo" + ) Output = AdyenPaymentsResult @staticmethod - def mutate(self, info, provider_id, transaction_reference, access_token, payment_method, browser_info): + def mutate( + self, + info, + provider_id, + transaction_reference, + access_token, + payment_method, + browser_info, + ): env = info.context["env"] - PaymentProvider = env['payment.provider'].sudo() - PaymentTransaction = env['payment.transaction'].sudo() - website = env['website'].get_current_website() + PaymentProvider = env["payment.provider"].sudo() + PaymentTransaction = env["payment.transaction"].sudo() + website = env["website"].get_current_website() request.website = website domain = [ - ('id', '=', provider_id), - ('state', 'in', ['enabled', 'test']), + ("id", "=", provider_id), + ("state", "in", ["enabled", "test"]), ] payment_provider_id = PaymentProvider.search(domain, limit=1) if not payment_provider_id: - raise GraphQLError(_('Payment Provider does not exist.')) + raise GraphQLError(_("Payment Provider does not exist.")) - if not payment_provider_id.code == 'adyen': + if not payment_provider_id.code == "adyen": raise GraphQLError(_('Payment Provider "Adyen" does not exist.')) - transaction = PaymentTransaction.search([('reference', '=', transaction_reference)], limit=1) + transaction = PaymentTransaction.search( + [("reference", "=", transaction_reference)], limit=1 + ) if not transaction: - raise GraphQLError(_('Payment transaction does not exist.')) + raise GraphQLError(_("Payment transaction does not exist.")) converted_amount = payment_utils.to_minor_currency_units( transaction.amount, transaction.currency_id, - arbitrary_decimal_number=CURRENCY_DECIMALS.get(transaction.currency_id.name, 2) + arbitrary_decimal_number=CURRENCY_DECIMALS.get( + transaction.currency_id.name, 2 + ), ) # Create Payment @@ -313,7 +351,7 @@ def mutate(self, info, provider_id, transaction_reference, access_token, payment partner_id=transaction.partner_id.id, payment_method=payment_method, access_token=access_token, - browser_info=browser_info + browser_info=browser_info, ) return AdyenPaymentsResult(adyen_payments=adyen_payment) @@ -323,46 +361,56 @@ class AdyenPaymentDetails(graphene.Mutation): class Arguments: provider_id = graphene.Int(required=True) transaction_reference = graphene.String(required=True) - payment_details = generic.GenericScalar(required=True, description='Return state.data') + payment_details = generic.GenericScalar( + required=True, description="Return state.data" + ) Output = AdyenPaymentDetailsResult @staticmethod def mutate(self, info, provider_id, transaction_reference, payment_details): env = info.context["env"] - PaymentProvider = env['payment.provider'].sudo() - PaymentTransaction = env['payment.transaction'].sudo() - website = env['website'].get_current_website() + PaymentProvider = env["payment.provider"].sudo() + PaymentTransaction = env["payment.transaction"].sudo() + website = env["website"].get_current_website() request.website = website domain = [ - ('id', '=', provider_id), - ('state', 'in', ['enabled', 'test']), + ("id", "=", provider_id), + ("state", "in", ["enabled", "test"]), ] payment_provider_id = PaymentProvider.search(domain, limit=1) if not payment_provider_id: - raise GraphQLError(_('Payment Provider does not exist.')) + raise GraphQLError(_("Payment Provider does not exist.")) - if not payment_provider_id.code == 'adyen': + if not payment_provider_id.code == "adyen": raise GraphQLError(_('Payment Provider "Adyen" does not exist.')) - transaction = PaymentTransaction.search([('reference', '=', transaction_reference)], limit=1) + transaction = PaymentTransaction.search( + [("reference", "=", transaction_reference)], limit=1 + ) if not transaction: - raise GraphQLError(_('Payment transaction does not exist.')) + raise GraphQLError(_("Payment transaction does not exist.")) # Submit the details adyen_payment_details = AdyenController().adyen_payment_details( provider_id=payment_provider_id.id, reference=transaction.reference, - payment_details=payment_details + payment_details=payment_details, ) return AdyenPaymentDetailsResult(adyen_payment_details=adyen_payment_details) class AdyenPaymentMutation(graphene.ObjectType): - adyen_provider_info = AdyenProviderInfo.Field(description='Get Adyen Provider Info.') - adyen_payment_methods = AdyenPaymentMethods.Field(description='Get Adyen Payment Methods.') - adyen_transaction = AdyenTransaction.Field(description='Create Adyen Transaction') - adyen_payments = AdyenPayments.Field(description='Make Adyen Payment request.') - adyen_payment_details = AdyenPaymentDetails.Field(description='Submit the Adyen Payment Details.') + adyen_provider_info = AdyenProviderInfo.Field( + description="Get Adyen Provider Info." + ) + adyen_payment_methods = AdyenPaymentMethods.Field( + description="Get Adyen Payment Methods." + ) + adyen_transaction = AdyenTransaction.Field(description="Create Adyen Transaction") + adyen_payments = AdyenPayments.Field(description="Make Adyen Payment request.") + adyen_payment_details = AdyenPaymentDetails.Field( + description="Submit the Adyen Payment Details." + ) diff --git a/vuestorefront/schemas/product.py b/vuestorefront/schemas/product.py new file mode 100644 index 0000000..a839c1f --- /dev/null +++ b/vuestorefront/schemas/product.py @@ -0,0 +1,205 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import graphene +from graphql import GraphQLError + +from odoo import _ +from odoo.http import request + +from ..utils import get_offset +from .objects import Attribute, AttributeValue, Product, SortEnum + + +def get_search_order(sort): + sorting = "" + for field, val in sort.items(): + if sorting: + sorting += ", " + if field == "price": + sorting += "list_price %s" % val.value + else: + sorting += "%s %s" % (field, val.value) + + # Add id as last factor, so we can consistently get the same results + if sorting: + sorting += ", id ASC" + else: + sorting = "id ASC" + + return sorting + + +def get_product_list(env, current_page, page_size, search, sort, **kwargs): + Product = env["product.template"].sudo() + domain = Product.prepare_vsf_domain(search, **kwargs) + offset = get_offset(current_page, page_size) + order = get_search_order(sort) + products = Product.search(domain, order=order, offset=offset, limit=page_size) + # Total count from the whole query is needed for creating proper pagination for it + total_count = Product.search_count(domain) + ProductAttributeValue = env["product.attribute.value"] + # TODO: either return standard attribute value from products or + # redesign to not return anything instead of empty recordset ( + # ProductAttributeValue). + # TODO: return some product prices instead of `0.0`. + return products, total_count, ProductAttributeValue, 0.0, 0.0 + + +class Products(graphene.Interface): + products = graphene.List(Product) + total_count = graphene.Int(required=True) + attribute_values = graphene.List(AttributeValue) + min_price = graphene.Float() + max_price = graphene.Float() + + +class ProductList(graphene.ObjectType): + class Meta: + interfaces = (Products,) + + +class ProductFilterInput(graphene.InputObjectType): + ids = graphene.List(graphene.Int) + category_id = graphene.List(graphene.Int) + category_slug = graphene.String() + # Deprecated + attribute_value_id = graphene.List(graphene.Int) + attrib_values = graphene.List(graphene.String) + name = graphene.String() + min_price = graphene.Float() + max_price = graphene.Float() + + +class ProductSortInput(graphene.InputObjectType): + id = SortEnum() + name = SortEnum() + price = SortEnum() + + +class ProductVariant(graphene.Interface): + product = graphene.Field(Product) + product_template_id = graphene.Int() + display_name = graphene.String() + display_image = graphene.Boolean() + price = graphene.Float() + list_price = graphene.String() + has_discounted_price = graphene.Boolean() + is_combination_possible = graphene.Boolean() + + +class ProductVariantData(graphene.ObjectType): + class Meta: + interfaces = (ProductVariant,) + + +class ProductQuery(graphene.ObjectType): + product = graphene.Field( + Product, + id=graphene.Int(default_value=None), + slug=graphene.String(default_value=None), + barcode=graphene.String(default_value=None), + ) + products = graphene.Field( + Products, + filter=graphene.Argument(ProductFilterInput, default_value={}), + current_page=graphene.Int(default_value=1), + page_size=graphene.Int(default_value=20), + search=graphene.String(default_value=False), + sort=graphene.Argument(ProductSortInput, default_value={}), + ) + attribute = graphene.Field( + Attribute, + required=True, + id=graphene.Int(), + ) + product_variant = graphene.Field( + ProductVariant, + required=True, + product_template_id=graphene.Int(), + combination_id=graphene.List(graphene.Int), + ) + + @staticmethod + def resolve_product(self, info, id=None, slug=None, barcode=None): + env = info.context["env"] + Product = env["product.template"].sudo() + + if id: + product = Product.search([("id", "=", id)], limit=1) + elif slug: + product = Product.search([("slug", "=", slug)], limit=1) + elif barcode: + product = Product.search([("barcode", "=", barcode)], limit=1) + else: + product = Product + + if product: + website = env["website"].get_current_website() + request.website = website + if not product.can_access_from_current_website(): + product = Product + + return product + + @staticmethod + def resolve_products(self, info, filter, current_page, page_size, search, sort): + env = info.context["env"] + ( + products, + total_count, + attribute_values, + min_price, + max_price, + ) = get_product_list(env, current_page, page_size, search, sort, **filter) + return ProductList( + products=products, + total_count=total_count, + attribute_values=attribute_values, + min_price=min_price, + max_price=max_price, + ) + + @staticmethod + def resolve_attribute(self, info, id): + return info.context["env"]["product.attribute"].search( + [("id", "=", id)], limit=1 + ) + + @staticmethod + def resolve_product_variant(self, info, product_template_id, combination_id): + env = info.context["env"] + + website = env["website"].get_current_website() + request.website = website + pricelist = website.get_current_pricelist() + + product_template = env["product.template"].browse(product_template_id) + combination = env["product.template.attribute.value"].browse(combination_id) + + variant_info = product_template._get_combination_info(combination, pricelist) + + product = env["product.product"].browse(variant_info["product_id"]) + + # Condition to verify if Product exist + if not product: + raise GraphQLError(_("Product does not exist")) + + is_combination_possible = product_template._is_combination_possible(combination) + + # Condition to Verify if Product is active or if combination exist + if not product.active or not is_combination_possible: + variant_info["is_combination_possible"] = False + else: + variant_info["is_combination_possible"] = True + + return ProductVariantData( + product=product, + product_template_id=variant_info["product_template_id"], + display_name=variant_info["display_name"], + display_image=variant_info["display_image"], + price=variant_info["price"], + list_price=variant_info["list_price"], + has_discounted_price=variant_info["has_discounted_price"], + is_combination_possible=variant_info["is_combination_possible"], + ) diff --git a/graphql_vuestorefront/schemas/shop.py b/vuestorefront/schemas/shop.py similarity index 70% rename from graphql_vuestorefront/schemas/shop.py rename to vuestorefront/schemas/shop.py index 9dec15e..ecb2016 100644 --- a/graphql_vuestorefront/schemas/shop.py +++ b/vuestorefront/schemas/shop.py @@ -1,12 +1,18 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene -from odoo.addons.graphql_vuestorefront.schemas.objects import Order, Partner -from odoo.addons.website_mass_mailing.controllers.main import MassMailController + from odoo.http import request +from odoo.addons.website_mass_mailing.controllers.main import MassMailController + +from .objects import Order, Partner + + +def predicate_order_line_id(line_id): + return lambda r: r.id == line_id + class Cart(graphene.Interface): order = graphene.Field(Order) @@ -25,14 +31,14 @@ class ShoppingCartQuery(graphene.ObjectType): @staticmethod def resolve_cart(self, info): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=True) - if order and order.state != 'draft': - request.session['sale_order_id'] = None + if order and order.state != "draft": + request.session["sale_order_id"] = None order = website.sale_get_order(force_create=True) if order: - order.order_line.filtered(lambda l: not l.product_id.active).unlink() + order.order_line.filtered(lambda r: not r.product_id.active).unlink() return CartData(order=order) @@ -46,11 +52,11 @@ class Arguments: @staticmethod def mutate(self, info, product_id, quantity): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) # Forcing the website_id to be passed to the Order - order.write({'website_id': website.id}) + order.write({"website_id": website.id}) order._cart_update(product_id=product_id, add_qty=quantity) return CartData(order=order) @@ -65,13 +71,15 @@ class Arguments: @staticmethod def mutate(self, info, line_id, quantity): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) line = order.order_line.filtered(lambda rec: rec.id == line_id) # Reset Warning Stock Message always before a new update line.shop_warning = "" - order._cart_update(product_id=line.product_id.id, line_id=line.id, set_qty=quantity) + order._cart_update( + product_id=line.product_id.id, line_id=line.id, set_qty=quantity + ) return CartData(order=order) @@ -84,7 +92,7 @@ class Arguments: @staticmethod def mutate(self, info, line_id): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) line = order.order_line.filtered(lambda rec: rec.id == line_id) @@ -98,7 +106,7 @@ class CartClear(graphene.Mutation): @staticmethod def mutate(self, info): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) order.order_line.sudo().unlink() @@ -114,7 +122,7 @@ class Arguments: @staticmethod def mutate(self, info, shipping_method_id): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) @@ -127,6 +135,7 @@ def mutate(self, info, shipping_method_id): # Additional Mutations that can be useful # # ---------------------------------------------------# + class ProductInput(graphene.InputObjectType): id = graphene.Int(required=True) quantity = graphene.Int(required=True) @@ -146,14 +155,14 @@ class Arguments: @staticmethod def mutate(self, info, products): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) # Forcing the website_id to be passed to the Order - order.write({'website_id': website.id}) + order.write({"website_id": website.id}) for product in products: - product_id = product['id'] - quantity = product['quantity'] + product_id = product["id"] + quantity = product["quantity"] order._cart_update(product_id=product_id, add_qty=quantity) return CartData(order=order) @@ -167,16 +176,18 @@ class Arguments: @staticmethod def mutate(self, info, lines): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) for line in lines: - line_id = line['id'] - quantity = line['quantity'] - line = order.order_line.filtered(lambda rec: rec.id == line_id) + line_id = line["id"] + quantity = line["quantity"] + line = order.order_line.filtered(predicate_order_line_id(line_id)) # Reset Warning Stock Message always before a new update line.shop_warning = "" - order._cart_update(product_id=line.product_id.id, line_id=line.id, set_qty=quantity) + order._cart_update( + product_id=line.product_id.id, line_id=line.id, set_qty=quantity + ) return CartData(order=order) @@ -189,11 +200,11 @@ class Arguments: @staticmethod def mutate(self, info, line_ids): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) for line_id in line_ids: - line = order.order_line.filtered(lambda rec: rec.id == line_id) + line = order.order_line.filtered(predicate_order_line_id(line_id)) line.unlink() return CartData(order=order) @@ -208,34 +219,38 @@ class Arguments: @staticmethod def mutate(self, info, name, email, subscribe_newsletter): - env = info.context['env'] - website = env['website'].get_current_website() + env = info.context["env"] + website = env["website"].get_current_website() request.website = website order = website.sale_get_order(force_create=1) data = { - 'name': name, - 'email': email, + "name": name, + "email": email, } partner = order.partner_id # Is public user if partner.id == website.user_id.sudo().partner_id.id: - partner = env['res.partner'].sudo().create(data) - - order.write({ - 'partner_id': partner.id, - 'partner_invoice_id': partner.id, - 'partner_shipping_id': partner.id, - }) + partner = env["res.partner"].sudo().create(data) + + order.write( + { + "partner_id": partner.id, + "partner_invoice_id": partner.id, + "partner_shipping_id": partner.id, + } + ) else: partner.write(data) # Subscribe to newsletter if subscribe_newsletter: if website.vsf_mailing_list_id: - MassMailController().subscribe(website.vsf_mailing_list_id.id, email, 'email') + MassMailController().subscribe( + website.vsf_mailing_list_id.id, email, "email" + ) return partner @@ -245,8 +260,18 @@ class ShopMutation(graphene.ObjectType): cart_update_item = CartUpdateItem.Field(description="Update Item") cart_remove_item = CartRemoveItem.Field(description="Remove Item") cart_clear = CartClear.Field(description="Cart Clear") - cart_add_multiple_items = CartAddMultipleItems.Field(description="Add Multiple Items") - cart_update_multiple_items = CartUpdateMultipleItems.Field(description="Update Multiple Items") - cart_remove_multiple_items = CartRemoveMultipleItems.Field(description="Remove Multiple Items") - set_shipping_method = SetShippingMethod.Field(description="Set Shipping Method on Cart") - create_update_partner = CreateUpdatePartner.Field(description="Create or update a partner for guest checkout") + cart_add_multiple_items = CartAddMultipleItems.Field( + description="Add Multiple Items" + ) + cart_update_multiple_items = CartUpdateMultipleItems.Field( + description="Update Multiple Items" + ) + cart_remove_multiple_items = CartRemoveMultipleItems.Field( + description="Remove Multiple Items" + ) + set_shipping_method = SetShippingMethod.Field( + description="Set Shipping Method on Cart" + ) + create_update_partner = CreateUpdatePartner.Field( + description="Create or update a partner for guest checkout" + ) diff --git a/graphql_vuestorefront/schemas/sign.py b/vuestorefront/schemas/sign.py similarity index 56% rename from graphql_vuestorefront/schemas/sign.py rename to vuestorefront/schemas/sign.py index b0d0f96..08bd110 100644 --- a/graphql_vuestorefront/schemas/sign.py +++ b/vuestorefront/schemas/sign.py @@ -1,17 +1,23 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphql import GraphQLError + import odoo from odoo import _ -from odoo.http import request from odoo.exceptions import UserError +from odoo.http import request + from odoo.addons.auth_signup.models.res_users import SignupError -from odoo.addons.graphql_vuestorefront.schemas.objects import User from odoo.addons.website_mass_mailing.controllers.main import MassMailController +from .objects import User + + +class UserRegisterExtraInput(graphene.InputObjectType): + vat = graphene.String() + class Login(graphene.Mutation): class Arguments: @@ -23,8 +29,8 @@ class Arguments: @staticmethod def mutate(self, info, email, password, subscribe_newsletter): - env = info.context['env'] - website = env['website'].get_current_website() + env = info.context["env"] + website = env["website"].get_current_website() request.website = website # Set email in lowercase @@ -34,11 +40,13 @@ def mutate(self, info, email, password, subscribe_newsletter): uid = request.session.authenticate(request.session.db, email, password) # Subscribe Newsletter if website and website.vsf_mailing_list_id and subscribe_newsletter: - MassMailController().subscribe(website.vsf_mailing_list_id.id, email, 'email') - return env['res.users'].sudo().browse(uid) + MassMailController().subscribe( + website.vsf_mailing_list_id.id, email, "email" + ) + return env["res.users"].sudo().browse(uid) except odoo.exceptions.AccessDenied as e: if e.args == odoo.exceptions.AccessDenied().args: - raise GraphQLError(_('Wrong email or password.')) + raise GraphQLError(_("Wrong email or password.")) else: raise GraphQLError(_(e.args[0])) @@ -59,34 +67,30 @@ class Arguments: email = graphene.String(required=True) password = graphene.String(required=True) subscribe_newsletter = graphene.Boolean(default_value=False) + extra = UserRegisterExtraInput(default_value={}) Output = User @staticmethod - def mutate(self, info, name, email, password, subscribe_newsletter): - env = info.context['env'] - website = env['website'].get_current_website() + def mutate(self, info, name, email, password, subscribe_newsletter, extra): + env = info.context["env"] + ResUsers = env["res.users"] + website = env["website"].get_current_website() request.website = website - # Set email in lowercase email = email.lower() - - data = { - 'name': name, - 'login': email, - 'password': password, - } - - if env['res.users'].sudo().search([('login', '=', data['login'])], limit=1): - raise GraphQLError(_('Another user is already registered using this email address.')) - - env['res.users'].sudo().signup(data) - + data = ResUsers.prepare_vsf_signup_vals(name, email, password, **extra) + if ResUsers.sudo().search_count([("login", "=", data["login"])], limit=1): + raise GraphQLError( + _("Another user is already registered using this email address.") + ) + env["res.users"].sudo().signup(data) # Subscribe Newsletter if website and website.vsf_mailing_list_id and subscribe_newsletter: - MassMailController().subscribe(website.vsf_mailing_list_id.id, email, 'email') - - return env['res.users'].sudo().search([('login', '=', data['login'])], limit=1) + MassMailController().subscribe( + website.vsf_mailing_list_id.id, email, "email" + ) + return env["res.users"].sudo().search([("login", "=", data["login"])], limit=1) class ResetPassword(graphene.Mutation): @@ -97,18 +101,18 @@ class Arguments: @staticmethod def mutate(self, info, email): - env = info.context['env'] - ResUsers = env['res.users'].sudo() - create_user = info.context.get('create_user', False) + env = info.context["env"] + ResUsers = env["res.users"].sudo() + create_user = info.context.get("create_user", False) # Set email in lowercase email = email.lower() - user = ResUsers.search([('login', '=', email)]) + user = ResUsers.search([("login", "=", email)]) if not user: - user = ResUsers.search([('email', '=', email)]) + user = ResUsers.search([("email", "=", email)]) if len(user) != 1: - raise GraphQLError(_('Invalid email.')) + raise GraphQLError(_("Invalid email.")) try: user.with_context(create_user=create_user).api_action_reset_password() @@ -116,7 +120,7 @@ def mutate(self, info, email): except UserError as e: raise GraphQLError(e.name or e.value) except SignupError: - raise GraphQLError(_('Could not reset your password.')) + raise GraphQLError(_("Could not reset your password.")) except Exception as e: raise GraphQLError(str(e)) @@ -130,21 +134,21 @@ class Arguments: @staticmethod def mutate(self, info, token, new_password): - env = info.context['env'] + env = info.context["env"] data = { - 'password': new_password, + "password": new_password, } - ResUsers = env['res.users'].sudo() + ResUsers = env["res.users"].sudo() try: login, password = ResUsers.signup(data, token) - return ResUsers.search([('login', '=', login)], limit=1) + return ResUsers.search([("login", "=", login)], limit=1) except UserError as e: raise GraphQLError(e.args[0]) except SignupError: - raise GraphQLError(_('Could not change your password.')) + raise GraphQLError(_("Could not change your password.")) except Exception as e: raise GraphQLError(str(e)) @@ -158,34 +162,50 @@ class Arguments: @staticmethod def mutate(self, info, current_password, new_password): - env = info.context['env'] - website = env['website'].get_current_website() + env = info.context["env"] + website = env["website"].get_current_website() request.website = website website_user = website.user_id if env.uid: - user = env['res.users'].sudo().search([('id', '=', env.uid), ('active', 'in', [True, False])], limit=1) + user = ( + env["res.users"] + .sudo() + .search( + [("id", "=", env.uid), ("active", "in", [True, False])], limit=1 + ) + ) # Prevent "Public User" to be Updated if user and user.id and user.id == website_user.id: - raise GraphQLError(_('Partner cannot be updated.')) + raise GraphQLError(_("Partner cannot be updated.")) try: user._check_credentials(current_password, env) user.change_password(current_password, new_password) env.cr.commit() - request.session.authenticate(request.session.db, user.login, new_password) + request.session.authenticate( + request.session.db, user.login, new_password + ) return user except odoo.exceptions.AccessDenied: - raise GraphQLError(_('Incorrect password.')) + raise GraphQLError(_("Incorrect password.")) else: - raise GraphQLError(_('You must be logged in.')) + raise GraphQLError(_("You must be logged in.")) class SignMutation(graphene.ObjectType): - login = Login.Field(description='Authenticate user with email and password and retrieves token.') - logout = Logout.Field(description='Logout user') - register = Register.Field(description='Register a new user with email, name and password.') - reset_password = ResetPassword.Field(description="Send change password url to user's email.") - change_password = ChangePassword.Field(description="Set new user's password with the token from the change " - "password url received in the email.") + login = Login.Field( + description="Authenticate user with email and password and retrieves token." + ) + logout = Logout.Field(description="Logout user") + register = Register.Field( + description="Register a new user with email, name and password." + ) + reset_password = ResetPassword.Field( + description="Send change password url to user's email." + ) + change_password = ChangePassword.Field( + description="Set new user's password with the token from the change " + "password url received in the email." + ) update_password = UpdatePassword.Field(description="Update user password.") diff --git a/graphql_vuestorefront/schemas/user_profile.py b/vuestorefront/schemas/user_profile.py similarity index 70% rename from graphql_vuestorefront/schemas/user_profile.py rename to vuestorefront/schemas/user_profile.py index 53713f3..707b8d0 100644 --- a/graphql_vuestorefront/schemas/user_profile.py +++ b/vuestorefront/schemas/user_profile.py @@ -1,13 +1,13 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphql import GraphQLError + from odoo import _ from odoo.http import request -from odoo.addons.graphql_vuestorefront.schemas.objects import Partner +from .objects import Partner class UserProfileQuery(graphene.ObjectType): @@ -19,13 +19,13 @@ class UserProfileQuery(graphene.ObjectType): @staticmethod def resolve_partner(self, info): uid = request.session.uid - user = info.context['env']['res.users'].sudo().browse(uid) + user = info.context["env"]["res.users"].sudo().browse(uid) if user: partner = user.partner_id if not partner: - raise GraphQLError(_('Partner does not exist.')) + raise GraphQLError(_("Partner does not exist.")) else: - raise GraphQLError(_('User does not exist.')) + raise GraphQLError(_("User does not exist.")) return partner @@ -45,22 +45,22 @@ class Arguments: @staticmethod def mutate(self, info, myaccount): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website user = request.env.user website_user = website.user_id # Prevent "Public User" to be Updated if user.id == website_user.id: - raise GraphQLError(_('Partner cannot be updated.')) + raise GraphQLError(_("Partner cannot be updated.")) partner = user.partner_id if partner: partner.write(myaccount) else: - raise GraphQLError(_('Partner does not exist.')) + raise GraphQLError(_("Partner does not exist.")) return partner class UserProfileMutation(graphene.ObjectType): - update_my_account = UpdateMyAccount.Field(description='Update MyAccount') + update_my_account = UpdateMyAccount.Field(description="Update MyAccount") diff --git a/vuestorefront/schemas/website.py b/vuestorefront/schemas/website.py new file mode 100644 index 0000000..d337b9a --- /dev/null +++ b/vuestorefront/schemas/website.py @@ -0,0 +1,59 @@ +# Copyright 2023 ODOOGAP/PROMPTEQUATION LDA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import graphene + +from .objects import WebsiteMenu + + +class WebsiteQuery(graphene.ObjectType): + website_menu = graphene.List( + graphene.NonNull(WebsiteMenu), + ) + website_mega_menu = graphene.List( + graphene.NonNull(WebsiteMenu), + ) + website_footer = graphene.List( + graphene.NonNull(WebsiteMenu), + ) + + @staticmethod + def resolve_website_menu(self, info): + env = info.context["env"] + website = env["website"].get_current_website() + + domain = [ + ("website_id", "=", website.id), + ("is_visible", "=", True), + ("is_footer", "=", False), + ("is_mega_menu", "=", False), + ] + + return env["website.menu"].search(domain) + + @staticmethod + def resolve_website_mega_menu(self, info): + env = info.context["env"] + website = env["website"].get_current_website() + + domain = [ + ("website_id", "=", website.id), + ("is_visible", "=", True), + ("is_footer", "=", False), + ("is_mega_menu", "=", True), + ] + + return env["website.menu"].search(domain) + + @staticmethod + def resolve_website_footer(self, info): + env = info.context["env"] + website = env["website"].get_current_website() + + domain = [ + ("website_id", "=", website.id), + ("is_visible", "=", True), + ("is_footer", "=", True), + ("is_mega_menu", "=", False), + ] + + return env["website.menu"].search(domain) diff --git a/graphql_vuestorefront/schemas/wishlist.py b/vuestorefront/schemas/wishlist.py similarity index 68% rename from graphql_vuestorefront/schemas/wishlist.py rename to vuestorefront/schemas/wishlist.py index e5bf160..f392c92 100644 --- a/graphql_vuestorefront/schemas/wishlist.py +++ b/vuestorefront/schemas/wishlist.py @@ -1,14 +1,15 @@ -# -*- coding: utf-8 -*- # Copyright 2023 ODOOGAP/PROMPTEQUATION LDA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import graphene from graphql import GraphQLError -from odoo.http import request + from odoo import _ +from odoo.http import request from odoo.addons.website_sale_wishlist.controllers.main import WebsiteSaleWishlist -from odoo.addons.graphql_vuestorefront.schemas.objects import WishlistItem + +from .objects import WishlistItem class WishlistItems(graphene.Interface): @@ -28,11 +29,11 @@ class WishlistQuery(graphene.ObjectType): @staticmethod def resolve_wishlist_items(root, info): - """ Get current user wishlist items """ - env = info.context['env'] - website = env['website'].get_current_website() + """Get current user wishlist items.""" + env = info.context["env"] + website = env["website"].get_current_website() request.website = website - wishlist_items = env['product.wishlist'].current() + wishlist_items = env["product.wishlist"].current() total_count = len(wishlist_items) return WishlistData(wishlist_items=wishlist_items, total_count=total_count) @@ -46,37 +47,39 @@ class Arguments: @staticmethod def mutate(self, info, product_id): env = info.context["env"] - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website - values = env['product.wishlist'].with_context(display_default_code=False).current() + values = ( + env["product.wishlist"].with_context(display_default_code=False).current() + ) if values.filtered(lambda v: v.product_id.id == product_id): - raise GraphQLError(_('Product already exists in the Wishlist.')) + raise GraphQLError(_("Product already exists in the Wishlist.")) WebsiteSaleWishlist().add_to_wishlist(product_id) - wishlist_items = env['product.wishlist'].current() + wishlist_items = env["product.wishlist"].current() total_count = len(wishlist_items) return WishlistData(wishlist_items=wishlist_items, total_count=total_count) class WishlistRemoveItem(graphene.Mutation): class Arguments: - wish_id = graphene.Int(required=True) + wish_id = graphene.Int(required=True) Output = WishlistData @staticmethod def mutate(self, info, wish_id): - env = info.context['env'] - Wishlist = env['product.wishlist'].sudo() + env = info.context["env"] + Wishlist = env["product.wishlist"].sudo() - wish_id = Wishlist.search([('id', '=', wish_id)], limit=1) + wish_id = Wishlist.search([("id", "=", wish_id)], limit=1) wish_id.unlink() - website = env['website'].get_current_website() + website = env["website"].get_current_website() request.website = website - wishlist_items = env['product.wishlist'].current() + wishlist_items = env["product.wishlist"].current() total_count = len(wishlist_items) return WishlistData(wishlist_items=wishlist_items, total_count=total_count) diff --git a/vuestorefront/security/ir.model.access.csv b/vuestorefront/security/ir.model.access.csv new file mode 100644 index 0000000..7a14206 --- /dev/null +++ b/vuestorefront/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +vuestorefront.access_invalidate_cache,access_invalidate_cache,vuestorefront.model_invalidate_cache,base.group_user,1,1,1,1 +access_website_menu_image,access_website_menu_image,model_website_menu_image,,1,0,0,0 +access_website_menu_image_designer,access_website_menu_image_designer,vuestorefront.model_website_menu_image,website.group_website_designer,1,1,1,1 diff --git a/graphql_vuestorefront/static/description/icon.png b/vuestorefront/static/description/icon.png similarity index 100% rename from graphql_vuestorefront/static/description/icon.png rename to vuestorefront/static/description/icon.png diff --git a/graphql_vuestorefront/static/men/clothing/blazer/blazer-1.jpg b/vuestorefront/static/men/clothing/blazer/blazer-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/blazer/blazer-1.jpg rename to vuestorefront/static/men/clothing/blazer/blazer-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/blazer/blazer-2.jpg b/vuestorefront/static/men/clothing/blazer/blazer-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/blazer/blazer-2.jpg rename to vuestorefront/static/men/clothing/blazer/blazer-2.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-1-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-1-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-1-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-1-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-1-grey.jpg b/vuestorefront/static/men/clothing/jackets/jackets-1-grey.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-1-grey.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-1-grey.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-10-black.jpg b/vuestorefront/static/men/clothing/jackets/jackets-10-black.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-10-black.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-10-black.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-11-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-11-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-11-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-11-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-12-black.jpg b/vuestorefront/static/men/clothing/jackets/jackets-12-black.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-12-black.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-12-black.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-13-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-13-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-13-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-13-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-14-black.jpg b/vuestorefront/static/men/clothing/jackets/jackets-14-black.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-14-black.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-14-black.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-15-beige.jpg b/vuestorefront/static/men/clothing/jackets/jackets-15-beige.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-15-beige.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-15-beige.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-16-grey.jpg b/vuestorefront/static/men/clothing/jackets/jackets-16-grey.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-16-grey.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-16-grey.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-17-beige.jpg b/vuestorefront/static/men/clothing/jackets/jackets-17-beige.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-17-beige.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-17-beige.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-18-grey.jpg b/vuestorefront/static/men/clothing/jackets/jackets-18-grey.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-18-grey.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-18-grey.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-19-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-19-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-19-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-19-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-2-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-2-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-2-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-2-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-20-brown.jpg b/vuestorefront/static/men/clothing/jackets/jackets-20-brown.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-20-brown.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-20-brown.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-21-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-21-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-21-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-21-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-22-black.jpg b/vuestorefront/static/men/clothing/jackets/jackets-22-black.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-22-black.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-22-black.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-23-black.jpg b/vuestorefront/static/men/clothing/jackets/jackets-23-black.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-23-black.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-23-black.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-24-beige.jpg b/vuestorefront/static/men/clothing/jackets/jackets-24-beige.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-24-beige.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-24-beige.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-25-red.jpg b/vuestorefront/static/men/clothing/jackets/jackets-25-red.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-25-red.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-25-red.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-3-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-3-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-3-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-3-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-4-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-4-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-4-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-4-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-4-brown.jpg b/vuestorefront/static/men/clothing/jackets/jackets-4-brown.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-4-brown.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-4-brown.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-4-green.jpg b/vuestorefront/static/men/clothing/jackets/jackets-4-green.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-4-green.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-4-green.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-5-red.jpg b/vuestorefront/static/men/clothing/jackets/jackets-5-red.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-5-red.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-5-red.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-6-brown.jpg b/vuestorefront/static/men/clothing/jackets/jackets-6-brown.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-6-brown.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-6-brown.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-7-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-7-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-7-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-7-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-8-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-8-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-8-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-8-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jackets/jackets-9-blue.jpg b/vuestorefront/static/men/clothing/jackets/jackets-9-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jackets/jackets-9-blue.jpg rename to vuestorefront/static/men/clothing/jackets/jackets-9-blue.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jeans/jeans-1.jpg b/vuestorefront/static/men/clothing/jeans/jeans-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jeans/jeans-1.jpg rename to vuestorefront/static/men/clothing/jeans/jeans-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/jeans/jeans-2.jpg b/vuestorefront/static/men/clothing/jeans/jeans-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/jeans/jeans-2.jpg rename to vuestorefront/static/men/clothing/jeans/jeans-2.jpg diff --git a/graphql_vuestorefront/static/men/clothing/shirts/shirts-1.jpg b/vuestorefront/static/men/clothing/shirts/shirts-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/shirts/shirts-1.jpg rename to vuestorefront/static/men/clothing/shirts/shirts-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/shirts/shirts-2.jpg b/vuestorefront/static/men/clothing/shirts/shirts-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/shirts/shirts-2.jpg rename to vuestorefront/static/men/clothing/shirts/shirts-2.jpg diff --git a/graphql_vuestorefront/static/men/clothing/suits/suits-1.jpg b/vuestorefront/static/men/clothing/suits/suits-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/suits/suits-1.jpg rename to vuestorefront/static/men/clothing/suits/suits-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/suits/suits-2.jpg b/vuestorefront/static/men/clothing/suits/suits-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/suits/suits-2.jpg rename to vuestorefront/static/men/clothing/suits/suits-2.jpg diff --git a/graphql_vuestorefront/static/men/clothing/t-shirts/t-shirts-1.jpg b/vuestorefront/static/men/clothing/t-shirts/t-shirts-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/t-shirts/t-shirts-1.jpg rename to vuestorefront/static/men/clothing/t-shirts/t-shirts-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/t-shirts/t-shirts-2.jpg b/vuestorefront/static/men/clothing/t-shirts/t-shirts-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/t-shirts/t-shirts-2.jpg rename to vuestorefront/static/men/clothing/t-shirts/t-shirts-2.jpg diff --git a/graphql_vuestorefront/static/men/clothing/tops/tops-1.jpg b/vuestorefront/static/men/clothing/tops/tops-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/tops/tops-1.jpg rename to vuestorefront/static/men/clothing/tops/tops-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/tops/tops-2.jpg b/vuestorefront/static/men/clothing/tops/tops-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/tops/tops-2.jpg rename to vuestorefront/static/men/clothing/tops/tops-2.jpg diff --git a/graphql_vuestorefront/static/men/clothing/trousers/trousers-1.jpg b/vuestorefront/static/men/clothing/trousers/trousers-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/trousers/trousers-1.jpg rename to vuestorefront/static/men/clothing/trousers/trousers-1.jpg diff --git a/graphql_vuestorefront/static/men/clothing/trousers/trousers-2.jpg b/vuestorefront/static/men/clothing/trousers/trousers-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/clothing/trousers/trousers-2.jpg rename to vuestorefront/static/men/clothing/trousers/trousers-2.jpg diff --git a/graphql_vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-1.jpg b/vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-1.jpg rename to vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-1.jpg diff --git a/graphql_vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-2.jpg b/vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-2.jpg rename to vuestorefront/static/men/shoes/lace-up_shoes/lace-up_shoes-2.jpg diff --git a/graphql_vuestorefront/static/men/shoes/loafers/loafers-1.jpg b/vuestorefront/static/men/shoes/loafers/loafers-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/shoes/loafers/loafers-1.jpg rename to vuestorefront/static/men/shoes/loafers/loafers-1.jpg diff --git a/graphql_vuestorefront/static/men/shoes/loafers/loafers-2.jpg b/vuestorefront/static/men/shoes/loafers/loafers-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/shoes/loafers/loafers-2.jpg rename to vuestorefront/static/men/shoes/loafers/loafers-2.jpg diff --git a/graphql_vuestorefront/static/men/shoes/sneakers/sneakers-1.jpg b/vuestorefront/static/men/shoes/sneakers/sneakers-1.jpg similarity index 100% rename from graphql_vuestorefront/static/men/shoes/sneakers/sneakers-1.jpg rename to vuestorefront/static/men/shoes/sneakers/sneakers-1.jpg diff --git a/graphql_vuestorefront/static/men/shoes/sneakers/sneakers-2.jpg b/vuestorefront/static/men/shoes/sneakers/sneakers-2.jpg similarity index 100% rename from graphql_vuestorefront/static/men/shoes/sneakers/sneakers-2.jpg rename to vuestorefront/static/men/shoes/sneakers/sneakers-2.jpg diff --git a/graphql_vuestorefront/static/women/bags/clutches/clutches-1.jpg b/vuestorefront/static/women/bags/clutches/clutches-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/clutches/clutches-1.jpg rename to vuestorefront/static/women/bags/clutches/clutches-1.jpg diff --git a/graphql_vuestorefront/static/women/bags/clutches/clutches-2.jpg b/vuestorefront/static/women/bags/clutches/clutches-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/clutches/clutches-2.jpg rename to vuestorefront/static/women/bags/clutches/clutches-2.jpg diff --git a/graphql_vuestorefront/static/women/bags/clutches/clutches-3-blue.jpg b/vuestorefront/static/women/bags/clutches/clutches-3-blue.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/clutches/clutches-3-blue.jpg rename to vuestorefront/static/women/bags/clutches/clutches-3-blue.jpg diff --git a/graphql_vuestorefront/static/women/bags/clutches/clutches-3-brown.jpg b/vuestorefront/static/women/bags/clutches/clutches-3-brown.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/clutches/clutches-3-brown.jpg rename to vuestorefront/static/women/bags/clutches/clutches-3-brown.jpg diff --git a/graphql_vuestorefront/static/women/bags/clutches/clutches-3-pink.jpg b/vuestorefront/static/women/bags/clutches/clutches-3-pink.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/clutches/clutches-3-pink.jpg rename to vuestorefront/static/women/bags/clutches/clutches-3-pink.jpg diff --git a/graphql_vuestorefront/static/women/bags/clutches/clutches-3-yellow.jpg b/vuestorefront/static/women/bags/clutches/clutches-3-yellow.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/clutches/clutches-3-yellow.jpg rename to vuestorefront/static/women/bags/clutches/clutches-3-yellow.jpg diff --git a/graphql_vuestorefront/static/women/bags/handbag/handbag-1.jpg b/vuestorefront/static/women/bags/handbag/handbag-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/handbag/handbag-1.jpg rename to vuestorefront/static/women/bags/handbag/handbag-1.jpg diff --git a/graphql_vuestorefront/static/women/bags/handbag/handbag-2.jpg b/vuestorefront/static/women/bags/handbag/handbag-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/handbag/handbag-2.jpg rename to vuestorefront/static/women/bags/handbag/handbag-2.jpg diff --git a/graphql_vuestorefront/static/women/bags/shopper/shopper-1.jpg b/vuestorefront/static/women/bags/shopper/shopper-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/shopper/shopper-1.jpg rename to vuestorefront/static/women/bags/shopper/shopper-1.jpg diff --git a/graphql_vuestorefront/static/women/bags/shopper/shopper-2.jpg b/vuestorefront/static/women/bags/shopper/shopper-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/shopper/shopper-2.jpg rename to vuestorefront/static/women/bags/shopper/shopper-2.jpg diff --git a/graphql_vuestorefront/static/women/bags/shopper/shopper-3-black.jpg b/vuestorefront/static/women/bags/shopper/shopper-3-black.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/shopper/shopper-3-black.jpg rename to vuestorefront/static/women/bags/shopper/shopper-3-black.jpg diff --git a/graphql_vuestorefront/static/women/bags/shopper/shopper-3-white.jpg b/vuestorefront/static/women/bags/shopper/shopper-3-white.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/shopper/shopper-3-white.jpg rename to vuestorefront/static/women/bags/shopper/shopper-3-white.jpg diff --git a/graphql_vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-1.jpg b/vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-1.jpg rename to vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-1.jpg diff --git a/graphql_vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-2.jpg b/vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-2.jpg rename to vuestorefront/static/women/bags/shoulder_bags/shoulder_bags-2.jpg diff --git a/graphql_vuestorefront/static/women/bags/wallets/wallets-1.jpg b/vuestorefront/static/women/bags/wallets/wallets-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/wallets/wallets-1.jpg rename to vuestorefront/static/women/bags/wallets/wallets-1.jpg diff --git a/graphql_vuestorefront/static/women/bags/wallets/wallets-2.jpg b/vuestorefront/static/women/bags/wallets/wallets-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/bags/wallets/wallets-2.jpg rename to vuestorefront/static/women/bags/wallets/wallets-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/blazer/blazer-1.jpg b/vuestorefront/static/women/clothing/blazer/blazer-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/blazer/blazer-1.jpg rename to vuestorefront/static/women/clothing/blazer/blazer-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/blazer/blazer-2.jpg b/vuestorefront/static/women/clothing/blazer/blazer-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/blazer/blazer-2.jpg rename to vuestorefront/static/women/clothing/blazer/blazer-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/dresses/dresses-1.jpg b/vuestorefront/static/women/clothing/dresses/dresses-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/dresses/dresses-1.jpg rename to vuestorefront/static/women/clothing/dresses/dresses-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/dresses/dresses-2.jpg b/vuestorefront/static/women/clothing/dresses/dresses-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/dresses/dresses-2.jpg rename to vuestorefront/static/women/clothing/dresses/dresses-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/jackets/jacket-1.jpg b/vuestorefront/static/women/clothing/jackets/jacket-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/jackets/jacket-1.jpg rename to vuestorefront/static/women/clothing/jackets/jacket-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/jackets/jacket-2.jpg b/vuestorefront/static/women/clothing/jackets/jacket-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/jackets/jacket-2.jpg rename to vuestorefront/static/women/clothing/jackets/jacket-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/jeans/jeans-1.jpg b/vuestorefront/static/women/clothing/jeans/jeans-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/jeans/jeans-1.jpg rename to vuestorefront/static/women/clothing/jeans/jeans-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/jeans/jeans-2.jpg b/vuestorefront/static/women/clothing/jeans/jeans-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/jeans/jeans-2.jpg rename to vuestorefront/static/women/clothing/jeans/jeans-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/shirts/shirts-1.jpg b/vuestorefront/static/women/clothing/shirts/shirts-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/shirts/shirts-1.jpg rename to vuestorefront/static/women/clothing/shirts/shirts-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/shirts/shirts-2.jpg b/vuestorefront/static/women/clothing/shirts/shirts-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/shirts/shirts-2.jpg rename to vuestorefront/static/women/clothing/shirts/shirts-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/skirts/skirts-1.jpg b/vuestorefront/static/women/clothing/skirts/skirts-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/skirts/skirts-1.jpg rename to vuestorefront/static/women/clothing/skirts/skirts-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/skirts/skirts-2.jpg b/vuestorefront/static/women/clothing/skirts/skirts-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/skirts/skirts-2.jpg rename to vuestorefront/static/women/clothing/skirts/skirts-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/t-shirts/t-shirts-1.jpg b/vuestorefront/static/women/clothing/t-shirts/t-shirts-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/t-shirts/t-shirts-1.jpg rename to vuestorefront/static/women/clothing/t-shirts/t-shirts-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/t-shirts/t-shirts-2.jpg b/vuestorefront/static/women/clothing/t-shirts/t-shirts-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/t-shirts/t-shirts-2.jpg rename to vuestorefront/static/women/clothing/t-shirts/t-shirts-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/tops/tops-1.jpg b/vuestorefront/static/women/clothing/tops/tops-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/tops/tops-1.jpg rename to vuestorefront/static/women/clothing/tops/tops-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/tops/tops-2.jpg b/vuestorefront/static/women/clothing/tops/tops-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/tops/tops-2.jpg rename to vuestorefront/static/women/clothing/tops/tops-2.jpg diff --git a/graphql_vuestorefront/static/women/clothing/trouser/trouser-1.jpg b/vuestorefront/static/women/clothing/trouser/trouser-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/trouser/trouser-1.jpg rename to vuestorefront/static/women/clothing/trouser/trouser-1.jpg diff --git a/graphql_vuestorefront/static/women/clothing/trouser/trouser-2.jpg b/vuestorefront/static/women/clothing/trouser/trouser-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/clothing/trouser/trouser-2.jpg rename to vuestorefront/static/women/clothing/trouser/trouser-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/ankle_boots/ankle_boots-1.jpg b/vuestorefront/static/women/shoes/ankle_boots/ankle_boots-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/ankle_boots/ankle_boots-1.jpg rename to vuestorefront/static/women/shoes/ankle_boots/ankle_boots-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/ankle_boots/ankle_boots-2.jpg b/vuestorefront/static/women/shoes/ankle_boots/ankle_boots-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/ankle_boots/ankle_boots-2.jpg rename to vuestorefront/static/women/shoes/ankle_boots/ankle_boots-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/ballerinas/ballerinas-1.jpg b/vuestorefront/static/women/shoes/ballerinas/ballerinas-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/ballerinas/ballerinas-1.jpg rename to vuestorefront/static/women/shoes/ballerinas/ballerinas-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/ballerinas/ballerinas-2.jpg b/vuestorefront/static/women/shoes/ballerinas/ballerinas-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/ballerinas/ballerinas-2.jpg rename to vuestorefront/static/women/shoes/ballerinas/ballerinas-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/boots/boots-1.jpg b/vuestorefront/static/women/shoes/boots/boots-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/boots/boots-1.jpg rename to vuestorefront/static/women/shoes/boots/boots-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/boots/boots-2.jpg b/vuestorefront/static/women/shoes/boots/boots-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/boots/boots-2.jpg rename to vuestorefront/static/women/shoes/boots/boots-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/loafers/loafers-1.jpg b/vuestorefront/static/women/shoes/loafers/loafers-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/loafers/loafers-1.jpg rename to vuestorefront/static/women/shoes/loafers/loafers-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/loafers/loafers-2.jpg b/vuestorefront/static/women/shoes/loafers/loafers-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/loafers/loafers-2.jpg rename to vuestorefront/static/women/shoes/loafers/loafers-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/pumps/pumps-1.jpg b/vuestorefront/static/women/shoes/pumps/pumps-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/pumps/pumps-1.jpg rename to vuestorefront/static/women/shoes/pumps/pumps-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/pumps/pumps-2.jpg b/vuestorefront/static/women/shoes/pumps/pumps-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/pumps/pumps-2.jpg rename to vuestorefront/static/women/shoes/pumps/pumps-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/sandals/sandals-1.jpg b/vuestorefront/static/women/shoes/sandals/sandals-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/sandals/sandals-1.jpg rename to vuestorefront/static/women/shoes/sandals/sandals-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/sandals/sandals-2.jpg b/vuestorefront/static/women/shoes/sandals/sandals-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/sandals/sandals-2.jpg rename to vuestorefront/static/women/shoes/sandals/sandals-2.jpg diff --git a/graphql_vuestorefront/static/women/shoes/sneakers/sneakers-1.jpg b/vuestorefront/static/women/shoes/sneakers/sneakers-1.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/sneakers/sneakers-1.jpg rename to vuestorefront/static/women/shoes/sneakers/sneakers-1.jpg diff --git a/graphql_vuestorefront/static/women/shoes/sneakers/sneakers-2.jpg b/vuestorefront/static/women/shoes/sneakers/sneakers-2.jpg similarity index 100% rename from graphql_vuestorefront/static/women/shoes/sneakers/sneakers-2.jpg rename to vuestorefront/static/women/shoes/sneakers/sneakers-2.jpg diff --git a/vuestorefront/tests/__init__.py b/vuestorefront/tests/__init__.py new file mode 100644 index 0000000..5de4c01 --- /dev/null +++ b/vuestorefront/tests/__init__.py @@ -0,0 +1,10 @@ +from . import ( + test_public_category, + test_product, + test_product_query, + test_mutate_user, + test_mutate_shop, + test_query_category, + test_query_mutate_address, + test_query_mutate_order, +) diff --git a/vuestorefront/tests/common.py b/vuestorefront/tests/common.py new file mode 100644 index 0000000..2b8f30c --- /dev/null +++ b/vuestorefront/tests/common.py @@ -0,0 +1,104 @@ +from graphene.test import Client + +from odoo.tests.common import TransactionCase + +from ..schema import schema + + +class TestVuestorefrontCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Website = cls.env["website"] + cls.ProductProduct = cls.env["product.product"] + cls.ProductPublicCategory = cls.env["product.public.category"] + cls.ResUsers = cls.env["res.users"] + cls.ResPartner = cls.env["res.partner"] + cls.PaymentTransaction = cls.env["payment.transaction"] + cls.SaleOrder = cls.env["sale.order"] + cls.SaleOrderLine = cls.env["sale.order.line"] + cls.graphene_client = Client(schema) + cls.InvalidateCache = cls.env["invalidate.cache"] + cls.user_portal = cls.env.ref("base.demo_user0") + cls.partner_portal = cls.user_portal.partner_id + cls.public_category_desks = cls.env.ref("website_sale.public_category_desks") + cls.public_category_components = cls.env.ref( + "website_sale.public_category_desks_components" + ) + cls.public_category_bins = cls.env.ref("website_sale.public_category_bins") + cls.product_bin = cls.env.ref("product.product_product_9") + cls.product_tmpl_bin = cls.product_bin.product_tmpl_id + cls.country_lt = cls.env.ref("base.lt") + cls.website_1 = cls.env.ref("website.default_website") + cls.payment_acquirer_transfer = cls.env.ref("payment.payment_provider_transfer") + cls.product_tmpl_bin.is_published = True + + def execute(self, query, **kw): + res = self.graphene_client.execute(query, context={"env": self.env}, **kw) + if not res: + raise RuntimeError("GraphQL query returned no data") + if res.get("errors"): + raise RuntimeError( + "GraphQL query returned error: {}".format(repr(res["errors"])) + ) + return res.get("data") + + +class TestVuestorefrontSaleCommon(TestVuestorefrontCommon): + """Common class to have patched active sale order for portal user.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + ( + cls.partner_invoice_1, + cls.partner_delivery_1, + cls.partner_other_1, + ) = cls.ResPartner.create( + [ + { + "name": "Partner Invoice Address", + "street": "invoice 1", + "type": "invoice", + "parent_id": cls.partner_portal.id, + }, + { + "name": "Partner Delivery Address", + "street": "delivery 1", + "type": "delivery", + "parent_id": cls.partner_portal.id, + }, + { + "name": "Partner Other Address", + "street": "other 1", + "type": "other", + "parent_id": cls.partner_portal.id, + }, + ] + ) + + cls.sale_1 = cls.SaleOrder.create( + { + "partner_id": cls.partner_portal.id, + "partner_invoice_id": cls.partner_invoice_1.id, + "partner_shipping_id": cls.partner_delivery_1.id, + } + ) + cls.sale_1_line_1 = cls.SaleOrderLine.create( + { + "product_id": cls.product_bin.id, + "product_uom_qty": 10, + "order_id": cls.sale_1.id, + } + ) + cls.Website._patch_method( + "sale_get_order", + lambda *args, **kw: cls.sale_1.with_user(cls.user_portal).sudo(), + ) + # To give access to portal user. + cls.sale_1.message_subscribe(partner_ids=[cls.partner_portal.id]) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.Website._revert_method("sale_get_order") diff --git a/vuestorefront/tests/test_mutate_shop.py b/vuestorefront/tests/test_mutate_shop.py new file mode 100644 index 0000000..582d9af --- /dev/null +++ b/vuestorefront/tests/test_mutate_shop.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock, patch + +from ..schemas import shop +from .common import TestVuestorefrontSaleCommon + + +class TestMutateShop(TestVuestorefrontSaleCommon): + @patch.object(shop, "request", MagicMock()) + def test_01_mutate_add_to_cart(self): + # GIVEN + self.sale_1.order_line.unlink() + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + mutation CartAddItem ($productId: Int!) { + cartAddItem( + productId: $productId + quantity: 10 + ) { + order { + id + } + } + } + """, + variables={"productId": self.product_bin.id}, + ) + # THEN + self.assertEqual(res["cartAddItem"]["order"]["id"], self.sale_1.id) + self.assertEqual(self.sale_1.state, "draft") + sale_line_1 = self.sale_1.order_line + self.assertEqual(len(sale_line_1), 1) + self.assertEqual(sale_line_1.product_id, self.product_bin) + self.assertEqual(sale_line_1.product_uom_qty, 10) + # GIVEN + # Check if we can still query product on cart if it was + # unpublished after it was already in a cart. + self.product_bin.is_published = False + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + query getProductFromCart { + cart { + order { + orderLines { + product { + name + } + } + } + } + } + """, + ) + self.assertEqual( + res["cart"]["order"]["orderLines"], [{"product": {"name": "Pedal Bin"}}] + ) diff --git a/vuestorefront/tests/test_mutate_user.py b/vuestorefront/tests/test_mutate_user.py new file mode 100644 index 0000000..7c5874f --- /dev/null +++ b/vuestorefront/tests/test_mutate_user.py @@ -0,0 +1,36 @@ +from unittest.mock import MagicMock, patch + +from ..schemas import sign +from . import common + + +class TestMutateUser(common.TestVuestorefrontCommon): + @patch.object(sign, "request", MagicMock()) + def test_01_register_new_user(self): + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + mutation Register( + $name: String!, $email: String!, $password: String! + ) { + register ( + name: $name + email: $email + password: $password + extra: {vat: "XX123"} + ) { + id + } + } + """, + variables={ + "name": "my-name-1", + "email": "my-login-1", + "password": "my-password", # pragma: allowlist secret + }, + ) + # THEN + user = self.ResUsers.browse(res["register"]["id"]) + self.assertEqual(user.login, "my-login-1") + self.assertEqual(user.partner_id.vat, "XX123") diff --git a/vuestorefront/tests/test_product.py b/vuestorefront/tests/test_product.py new file mode 100644 index 0000000..a55e83d --- /dev/null +++ b/vuestorefront/tests/test_product.py @@ -0,0 +1,20 @@ +from ..schemas.product import get_product_list +from . import common + + +class TestProduct(common.TestVuestorefrontCommon): + def test_01_get_product_list_by_name(self): + # WHEN + res = get_product_list( + self.env, 1, 100, "", {}, name=self.product_tmpl_bin.name + ) + # THEN + self.assertEqual(res[0], self.product_tmpl_bin) + + def test_02_product_slug(self): + # GIVEN + pt = self.product_tmpl_bin + # WHEN + pt.name = "My product" + # THEN + self.assertEqual(pt.slug, f"my-product-{pt.id}") diff --git a/vuestorefront/tests/test_product_query.py b/vuestorefront/tests/test_product_query.py new file mode 100644 index 0000000..c6cb4da --- /dev/null +++ b/vuestorefront/tests/test_product_query.py @@ -0,0 +1,105 @@ +from . import common + + +class TestQueryProduct(common.TestVuestorefrontCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.public_category_bins.slug = "bins" + # Parent + cls.public_category_desks.slug = "desks" + # Child + cls.public_category_components.slug = "components" + + def test_01_query_product_archived(self): + # GIVEN + self.product_tmpl_bin.active = False + # WHEN + res = self.execute( + """ + query getProductTemplate($id: Int) { + product(id: $id) { + name + } + } + """, + variables={"id": self.product_tmpl_bin.id}, + ) + # THEN + self.assertEqual(res["product"], {"name": None}) + + def test_02_query_products_with_category_slug(self): + # WHEN + res = self.execute( + """ + query getProductTemplates($ids: [Int], $categorySlug: String) { + products(filter: {ids: $ids, categorySlug: $categorySlug}) { + products { + name + } + } + } + """, + variables={"ids": self.product_tmpl_bin.ids, "categorySlug": "bins"}, + ) + # THEN + self.assertEqual(res["products"]["products"], [{"name": "Pedal Bin"}]) + + def test_03_query_products_unpublished_with_category_slug(self): + # GIVEN + self.product_tmpl_bin.is_published = False + # WHEN + res = self.execute( + """ + query getProductTemplates($ids: [Int], $categorySlug: String) { + products(filter: {ids: $ids, categorySlug: $categorySlug}) { + products { + name + } + } + } + """, + variables={"ids": self.product_tmpl_bin.ids, "categorySlug": "bins"}, + ) + # THEN + self.assertEqual(res["products"]["products"], []) + + def test_04_query_products_with_category_slug_is_child(self): + # GIVEN + self.product_tmpl_bin.public_categ_ids |= self.public_category_components + # WHEN + res = self.execute( + """ + query getProductTemplates($ids: [Int], $categorySlug: String) { + products(filter: {ids: $ids, categorySlug: $categorySlug}) { + products { + name + } + } + } + """, + # Related with child, calling from parent. + variables={"ids": self.product_tmpl_bin.ids, "categorySlug": "desks"}, + ) + # THEN + self.assertEqual(res["products"]["products"], [{"name": "Pedal Bin"}]) + + def test_05_query_products_with_category_slug_is_parent(self): + # GIVEN + self.product_tmpl_bin.public_categ_ids |= self.public_category_desks + # WHEN + res = self.execute( + """ + query getProductTemplates($ids: [Int], $categorySlug: String) { + products(filter: {ids: $ids, categorySlug: $categorySlug}) { + products { + name + } + } + } + """, + # Related with parent, calling from child. + variables={"ids": self.product_tmpl_bin.ids, "categorySlug": "components"}, + ) + # THEN + self.assertEqual(res["products"]["products"], []) diff --git a/vuestorefront/tests/test_public_category.py b/vuestorefront/tests/test_public_category.py new file mode 100644 index 0000000..2ea4b1e --- /dev/null +++ b/vuestorefront/tests/test_public_category.py @@ -0,0 +1,33 @@ +from . import common + + +class TestPublicCategory(common.TestVuestorefrontCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_tmpl_bin.public_categ_ids |= cls.public_category_components + + def test_01_get_product_tags(self): + # WHEN + res = self.InvalidateCache._get_product_tags(self.product_tmpl_bin.ids) + # THEN + res = res.split(",") + self.assertEqual(len(res), 4) + self.assertIn(f"P{self.product_tmpl_bin.id}", res) + self.assertIn(f"C{self.public_category_bins.id}", res) + self.assertIn(f"C{self.public_category_components.id}", res) + self.assertIn(f"C{self.public_category_desks.id}", res) + + def test_02_compute_category_slug(self): + # WHEN + categ = self.ProductPublicCategory.create({"name": "my_categ"}) + # THEN + self.assertEqual(categ.slug, f"my-categ-{categ.id}") + + def test_03_set_category_slug_manually(self): + # WHEN + categ = self.ProductPublicCategory.create( + {"name": "my_categ", "slug": "custom-slug"} + ) + # THEN + self.assertEqual(categ.slug, "custom-slug") diff --git a/vuestorefront/tests/test_query_category.py b/vuestorefront/tests/test_query_category.py new file mode 100644 index 0000000..3dd03a7 --- /dev/null +++ b/vuestorefront/tests/test_query_category.py @@ -0,0 +1,20 @@ +from . import common + + +class TestQueryCategory(common.TestVuestorefrontCommon): + def test_01_query_categories(self): + # WHEN + res = self.execute( + """ + query getCategory($ids: [Int]) { + categories(filter: {ids: $ids}) { + categories { + name + } + } + } + """, + variables={"ids": self.public_category_bins.ids}, + ) + # THEN + self.assertEqual(res["categories"]["categories"], [{"name": "Bins"}]) diff --git a/vuestorefront/tests/test_query_mutate_address.py b/vuestorefront/tests/test_query_mutate_address.py new file mode 100644 index 0000000..23b9fa5 --- /dev/null +++ b/vuestorefront/tests/test_query_mutate_address.py @@ -0,0 +1,120 @@ +from unittest.mock import MagicMock, patch + +from ..schemas import address +from . import common + + +@patch.object(address, "request", MagicMock()) +class TestQueryMutateAddress(common.TestVuestorefrontSaleCommon): + def test_01_query_addresses_delivery(self): + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + query getAddresses($addressTypes: [AddressEnum]) { + addresses(filter: {addressTypes: $addressTypes}) { + name + street + } + } + """, + variables={"addressTypes": ["Shipping"]}, + ) + # THEN + self.assertEqual( + sorted(res["addresses"], key=lambda x: x["name"]), + sorted( + [ + { + "name": "Partner Delivery Address", + "street": "delivery 1", + }, + ], + key=lambda x: x["name"], + ), + ) + + def test_02_query_addresses_invoice_n_delivery(self): + # GIVEN + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + query getAddresses($addressTypes: [AddressEnum]) { + addresses(filter: {addressTypes: $addressTypes}) { + name + street + } + } + """, + variables={"addressTypes": ["Billing", "Shipping"]}, + ) + # THEN + self.assertEqual( + sorted(res["addresses"], key=lambda x: x["name"]), + sorted( + [ + { + "name": "Partner Invoice Address", + "street": "invoice 1", + }, + { + "name": "Partner Delivery Address", + "street": "delivery 1", + }, + ], + key=lambda x: x["name"], + ), + ) + + def test_03_mutate_create_delivery_address(self): + # GIVEN + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + mutation AddAddress($addressType: AddressEnum!, $countryId: Int!) { + addAddress ( + addressType: $addressType, + address: { + name: "Partner Delivery Address 2" + street: "delivery 2" + zip: "zip 2" + countryId: $countryId + phone: "123456" + } + ) { + id + } + } + """, + variables={"addressType": "Shipping", "countryId": self.country_lt.id}, + ) + # THEN + partner = self.ResPartner.browse(res["addAddress"]["id"]) + self.assertEqual(partner.street, "delivery 2") + self.assertEqual(self.sale_1.partner_shipping_id, partner) + + def test_04_mutate_update_delivery_address(self): + # GIVEN + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + mutation UpdateAddress($id: Int!) { + updateAddress ( + address: { + id: $id + street: "delivery 1 updated" + } + ) { + id + } + } + """, + variables={"id": self.partner_delivery_1.id}, + ) + # THEN + partner = self.ResPartner.browse(res["updateAddress"]["id"]) + self.assertEqual(partner, self.partner_delivery_1) + self.assertEqual(partner.street, "delivery 1 updated") diff --git a/vuestorefront/tests/test_query_mutate_order.py b/vuestorefront/tests/test_query_mutate_order.py new file mode 100644 index 0000000..2a2bd4e --- /dev/null +++ b/vuestorefront/tests/test_query_mutate_order.py @@ -0,0 +1,63 @@ +from unittest.mock import MagicMock, patch + +from ..schemas import order +from . import common + + +class TestQueryMutateOrder(common.TestVuestorefrontSaleCommon): + def test_01_query_order(self): + # GIVEN + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + query getOrder($id: Int) { + order(id: $id) { + name + clientOrderRef + orderLines { + product { + name + } + } + } + } + """, + variables={"id": self.sale_1.id}, + ) + # THEN + order_lines = res["order"].pop("orderLines") + self.assertEqual( + res["order"], + { + "name": self.sale_1.name, + "clientOrderRef": None, + }, + ) + self.assertEqual(order_lines[0]["product"]["name"], "Pedal Bin") + + @patch.object(order, "request", MagicMock()) + def test_02_mutate_order(self): + # GIVEN + # WHEN + with self.with_user("portal"): + res = self.execute( + """ + mutation { + updateOrder( + data: { + clientOrderRef: "abc123" + } + ) { + id + clientOrderRef + } + } + """, + ) + # THEN + self.assertEqual( + res["updateOrder"], + {"id": self.sale_1.id, "clientOrderRef": "abc123"}, + ) + self.assertEqual(self.sale_1.client_order_ref, "abc123") diff --git a/vuestorefront/utils.py b/vuestorefront/utils.py new file mode 100644 index 0000000..b61dc7c --- /dev/null +++ b/vuestorefront/utils.py @@ -0,0 +1,12 @@ +from odoo.addons.http_routing.models.ir_http import slugify + + +def slugify_with_sfx(value, sfx): + return f"{slugify(value)}{sfx}" + + +def get_offset(current_page, page_size): + # First offset is 0 but first page is 1 + if current_page > 1: + return (current_page - 1) * page_size + return 0 diff --git a/graphql_vuestorefront/views/product_views.xml b/vuestorefront/views/product_views.xml similarity index 92% rename from graphql_vuestorefront/views/product_views.xml rename to vuestorefront/views/product_views.xml index e70d69d..f4b692e 100644 --- a/graphql_vuestorefront/views/product_views.xml +++ b/vuestorefront/views/product_views.xml @@ -11,7 +11,7 @@ - +
@@ -22,7 +22,7 @@ - + diff --git a/graphql_vuestorefront/views/res_config_settings_views.xml b/vuestorefront/views/res_config_settings_views.xml similarity index 65% rename from graphql_vuestorefront/views/res_config_settings_views.xml rename to vuestorefront/views/res_config_settings_views.xml index 5a82854..484706b 100644 --- a/graphql_vuestorefront/views/res_config_settings_views.xml +++ b/vuestorefront/views/res_config_settings_views.xml @@ -14,7 +14,10 @@

Vue Storefront

-
+
@@ -26,7 +29,10 @@
-
+
-
+
-
+
@@ -54,7 +66,11 @@
-
+
-
+
-
+