[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    paths-ignore:\n      - \"**.css\"\n      - \"**.js\"\n      - \"**.md\"\n      - \"**.html\"\n      - \"**.csv\"\n  schedule:\n    # Run everday at midnight UTC / 5:30 IST\n    - cron: \"0 0 * * *\"\n\nenv:\n  WEBSHOP_BRANCH: ${{ github.base_ref || github.ref_name }}\n\nconcurrency:\n  group: develop-webshop-${{ github.event.number }}\n  cancel-in-progress: true\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n    name: Server\n    \n    services:\n      redis-cache:\n        image: redis:alpine\n        ports:\n          - 13000:6379\n      redis-queue:\n        image: redis:alpine\n        ports:\n          - 11000:6379\n      mariadb:\n        image: mariadb:10.6\n        env:\n          MYSQL_ROOT_PASSWORD: root\n        ports:\n          - 3306:3306\n        options: --health-cmd=\"mariadb-admin ping\" --health-interval=5s --health-timeout=2s --health-retries=3\n    \n    steps:\n      - name: Clone\n        uses: actions/checkout@v3\n      \n      - name: Find tests\n        run: |\n          echo \"Finding tests\"\n          grep -rn \"def test\" > /dev/null\n      \n      - name: Setup Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.10'\n      \n      - name: Setup Node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          check-latest: true\n      \n      - name: Cache pip\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }}\n          restore-keys: |\n            ${{ runner.os }}-pip-\n            ${{ runner.os }}-\n      \n      - name: Get yarn cache directory path\n        id: yarn-cache-dir-path\n        run: 'echo \"dir=$(yarn cache dir)\" >> $GITHUB_OUTPUT'\n      \n      - uses: actions/cache@v4\n        id: yarn-cache\n        with:\n          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}\n          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-\n      \n      - name: Install MariaDB Client\n        run: sudo apt update && sudo apt-get install mariadb-client\n      \n      - name: Setup\n        run: |\n          pip install frappe-bench\n          bench init --frappe-branch $WEBSHOP_BRANCH --skip-redis-config-generation --skip-assets --python \"$(which python)\" ~/frappe-bench\n          mariadb --host 127.0.0.1 --port 3306 -u root -proot -e \"SET GLOBAL character_set_server = 'utf8mb4'\"\n          mariadb --host 127.0.0.1 --port 3306 -u root -proot -e \"SET GLOBAL collation_server = 'utf8mb4_unicode_ci'\"\n      \n      - name: Install\n        working-directory: /home/runner/frappe-bench\n        run: |\n          bench setup requirements --dev\n          bench get-app erpnext --branch $WEBSHOP_BRANCH\n          bench get-app payments --branch $WEBSHOP_BRANCH\n          bench get-app $GITHUB_WORKSPACE\n          bench new-site --db-root-password root --admin-password admin test_site\n          bench --site test_site install-app erpnext\n          bench --site test_site install-app webshop\n          bench build\n        env:\n          CI: 'Yes'\n      \n      - name: Run Tests\n        working-directory: /home/runner/frappe-bench\n        run: |\n          bench --site test_site set-config allow_tests true\n          bench --site test_site run-tests --app webshop\n        env:\n          TYPE: server"
  },
  {
    "path": ".github/workflows/semgroup-rules.yml",
    "content": "name: Linters\n\non:\n  pull_request: { }\n\njobs:\n  linters:\n    name: Frappe Linter\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.10'\n\n      - name: Download Semgrep rules\n        run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules\n\n      - name: Download semgrep\n        run: pip install semgrep\n\n      - name: Run Semgrep rules\n        run: semgrep ci --config ./frappe-semgrep-rules/rules\n"
  },
  {
    "path": ".gitignore",
    "content": "**/__pycache__\n*.egg-info\n*.pyc\n*.py~\n*.swo\n*.swp\n*~\n.DS_Store\n.backportrc.json\n.idea/\n.vscode/\n.wnf-lang-status\n__pycache__\nconf.py\ndist/\nwebshop/docs/current\nwebshop/public/dist\nlatest_updates.json\nlocale\nnode_modules/\ntags\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Frappe Webshop\nFrappe Webshop is an Open Source eCommerce Platform, developed primarily using Python and JavaScript. Frappe Webshop was developed as part of the Frappe framework, which is designed to help developers quickly create business applications with minimal code by offering a combination of easy to use front-end and back-end development tools. Frappe Webshop is specifically targeted to help *small to medium sized businesses* with increasing their digital presence and being able to easily create online stores for their brands. Frappe Webshop can be integrated with ERPNext, which is an enterprise resource planning system used by businesses to record all their transactions in a single system. Frappe Webshop emphasizes simplicity, customization, and clean interfaces, providing businesses with the ability to create a tailored user experience both efficently and for free. \n## Table of Contents\n- [Table of Contents](https://github.com/frappe/webshop#table-of-contents)\n- [Why choose Frappe Webshop?](https://github.com/frappe/webshop/#why-choose-frappe-webshop)\n- [Example Frappe Webshop Interface](https://github.com/frappe/webshop/#example-frappe-webshop-interface)\n- [Features](https://github.com/frappe/webshop/#features)\n- [Setup](https://github.com/frappe/webshop/#setup)\n    - [Installation using Docker](https://github.com/frappe/webshop/#installation-using-docker)\n    - [Easy Install Script](https://github.com/frappe/webshop/#easy-install-script)\n    - [Manual Installation](https://github.com/frappe/webshop/#manual-installation)\n- [Usage](https://github.com/frappe/webshop/#usage)\n- [Contributing](https://github.com/frappe/webshop/#contributing)\n- [License](https://github.com/frappe/webshop/#license)\n\n## Why choose Frappe Webshop?\n- **Fast and Efficient**: Streamlined online sales process, offering a shopping experience that can quickly be developed to meet the needs of any business.\n\n- **Personalized and Customizable**: Webshops are highly customizable, providing features and designs that businesses choose and allowing for effective customer engagement with the products.\n\n- **Dynamic and Scalable**: Webshops can be easily adapted to fit business goals and to provide a seamless user experience for businesses small and large.\n\n- **Ease of use**: Wesbhop is designed to require minimal technical knowledge for developers, making it accessible for any business looking to develop an online commerce presence.\n\n- **Control and security**: Webshop is self-hosted, meaning that businesses have complete control over their own data and providing a secure environment for customer data.\n  \n- **Open source**: The Webshop platform is completely *free* to use, providing a cost-effective way for businesses to develop digital storefronts and market their brands online. Being open source also means that there are continuous improvements being made to the platform.\n\n## Example Frappe Webshop Interface\n![Frappe Webshop](webshop.png)\n\n## Features\n- **Product Management**: Add, edit, and manage products with the ability to add descriptions, images, variants, and inventory counts.\n    - **Inventory Control**: Real-time stock updates to ensure that product supply can be easily tracked.\n- **Multiple Payment Options**: Integration with popular payment options like Paypal or Stripe, allowing secure and convenient transactions.\n- **User Accounts**: Customers can create accounts and save their information, allowing for convenient checkout and personalized experiences.\n    - **Wishlist and Cart Functions**: Let customers save their favorites and revisit them later for easier purchases.\n    - **Order Tracking**: Keep customers informed and provide updates on the status of their orders.\n- **Advanced Search and Filters**: Advanced search tools and product filters to allow customers to quickly find products they want.\n- **Customer Reviews and Ratings**: Customers can share their experiences and product ratings.\n- **Integration with ERPNext**: Integration with ERPNext allows for management of inventory, billing, and order processing in one place.\n- **User-friendly interface**: Frappe Webshop emphasizes ease of use, with a focus on creating simple, yet effective websites.\n\n## Setup\n1. Install [bench](https://github.com/frappe/bench).\n   #### Installation using Docker\n   ```sh\n   $ git clone https://github.com/frappe/frappe_docker.git\n   $ cd frappe_docker\n   ```\n   See more details here: [Containerized Installation](https://github.com/frappe/bench#containerized-installation)\n   #### Easy Install Script\n   ```sh\n   $ wget https://raw.githubusercontent.com/frappe/bench/develop/easy-install.py\n   $ python3 easy-install.py --prod --email your@email.tld\n   ```\n   See more details here: [Easy Install Script](https://github.com/frappe/bench#easy-install-script)\n   #### Manual Installation (*recommended only for local development*)\n   ```sh\n   $ pip install frappe-bench\n   ```\n   See more details here: [Manual Installation](https://github.com/frappe/bench#manual-installation)\n\n   ##### Bench\n   More information on [usage of bench and its commands](https://github.com/frappe/bench#basic-usage).\n2. Install ERPNext (only required if bench was installed using manual installation).\n3. Once ERPNext is installed, add the webshop app to your bench by running\n\n    ```sh\n    $ bench get-app webshop\n    ```\n4. After that, you can install the webshop app on the required site by running\n    ```sh\n    $ bench --site sitename install-app webshop\n    ```\n\n## Usage\nOnce setup has been completed, eCommerce features can be set up. This [guide](https://docs.erpnext.com/docs/user/manual/en/set_up_e_commerce) explains how to begin setup of the eCommerce features in conjunction with ERPNext. Many common features and customization options are explained, providing a solid framework for users to get started with building their eCommerce platforms. Note that for best results, users should have some Items setup using ERPNext before attempting to create a store. Creating items is a simple process that can be done through the Webshop interface by accessing > Home > Stock > Items and Pricing > Item.\n\n## Contributing\nTo contribute to the development of the Frappe Webshop, please make a fork of this repository and make edits within the forked repository. Once satisfied that a contribution should be deployed, create a pull request from your forked repository to this repository. Changes that are accepted will be merged into the main development branch and thus be rolled out to users. To make changes using the bench command line interface, make a clone of this repo using the following command:\n```sh\n$ git clone https://github.com/frappe/webshop.git\n```\nFor more information on using the bench command line interface, please reference this [page](https://github.com/frappe/bench#development).\n\n## License\nLicensed under the GNU GENERAL PUBLIC LICENSE V3. This is an open-source project meant to help businesses create online commerce platforms. (See [LICENSE](LICENSE) for more information).\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"webshop\"\nauthors = [\n    { name = \"Frappe Technologies Pvt. Ltd.\", email = \"contact@frappe.io\"}\n]\ndescription = \"Open Source eCommerce Platform\"\nrequires-python = \">=3.10\"\nreadme = \"README.md\"\ndynamic = [\"version\"]\ndependencies = []\n\n[build-system]\nrequires = [\"flit_core >=3.4,<4\"]\nbuild-backend = \"flit_core.buildapi\"\n\n[tool.black]\nline-length = 99\n\n[tool.isort]\nline_length = 99\nmulti_line_output = 3\ninclude_trailing_comma = true\nforce_grid_wrap = 0\nuse_parentheses = true\nensure_newline_before_comments = true\nindent = \"\\t\"\n"
  },
  {
    "path": "webshop/__init__.py",
    "content": "\n__version__ = '0.0.1'\n\n"
  },
  {
    "path": "webshop/config/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/hooks.py",
    "content": "from . import __version__ as _version\n\napp_name = \"webshop\"\napp_title = \"Webshop\"\napp_publisher = \"Frappe Technologies Pvt. Ltd.\"\napp_description = \"Open Source eCommerce Platform\"\napp_email = \"contact@frappe.io\"\napp_license = \"GNU General Public License (v3)\"\napp_version = _version\n\nrequired_apps = [\"payments\", \"erpnext\"]\n\nweb_include_css = \"webshop-web.bundle.css\"\n\nweb_include_js = \"web.bundle.js\"\n\nafter_install = \"webshop.setup.install.after_install\"\non_logout = \"webshop.webshop.shopping_cart.utils.clear_cart_count\"\non_session_creation = [\n    \"webshop.webshop.utils.portal.update_debtors_account\",\n    \"webshop.webshop.shopping_cart.utils.set_cart_count\",\n]\nupdate_website_context = [\n    \"webshop.webshop.shopping_cart.utils.update_website_context\",\n]\n\nwebsite_generators = [\"Website Item\", \"Item Group\"]\n\noverride_doctype_class = {\n    \"Payment Request\": \"webshop.webshop.doctype.override_doctype.payment_request.PaymentRequest\",\n    \"Item Group\": \"webshop.webshop.doctype.override_doctype.item_group.WebshopItemGroup\",\n    \"Item\": \"webshop.webshop.doctype.override_doctype.item.WebshopItem\",\n}\n\ndoctype_js = {\n    \"Item\": \"public/js/override/item.js\",\n    \"Homepage\": \"public/js/override/homepage.js\",\n}\n\ndoc_events = {\n    \"Item\": {\n        \"on_update\": [\n            \"webshop.webshop.crud_events.item.update_website_item.execute\",\n            \"webshop.webshop.crud_events.item.invalidate_item_variants_cache.execute\",\n        ],\n        \"before_rename\": [\n            \"webshop.webshop.crud_events.item.validate_duplicate_website_item.execute\",\n        ],\n        \"after_rename\": [\n            \"webshop.webshop.crud_events.item.invalidate_item_variants_cache.execute\",\n        ],\n    },\n    \"Sales Taxes and Charges Template\": {\n        \"on_update\": [\n            \"webshop.webshop.doctype.webshop_settings.webshop_settings.validate_cart_settings\",\n        ],\n    },\n    \"Quotation\": {\n        \"validate\": [\n            \"webshop.webshop.crud_events.quotation.validate_shopping_cart_items.execute\",\n        ],\n    },\n    \"Price List\": {\n        \"validate\": [\n            \"webshop.webshop.crud_events.price_list.check_impact_on_cart.execute\"\n        ],\n    },\n    \"Tax Rule\": {\n        \"validate\": [\n            \"webshop.webshop.crud_events.tax_rule.validate_use_for_cart.execute\",\n        ],\n    },\n}\n\nhas_website_permission = {\n    \"Website Item\": \"webshop.webshop.doctype.website_item.website_item.has_website_permission_for_website_item\",\n    \"Item Group\": \"webshop.webshop.doctype.website_item.website_item.has_website_permission_for_item_group\"\n}\n"
  },
  {
    "path": "webshop/modules.txt",
    "content": "Webshop"
  },
  {
    "path": "webshop/patches/__init__.py",
    "content": "\n__version__ = '0.0.1'\n\n"
  },
  {
    "path": "webshop/patches/add_homepage_field.py",
    "content": "import frappe\nfrom frappe.custom.doctype.custom_field.custom_field import create_custom_fields\n\n\ndef execute():\n\tif not frappe.db.exists(\"DocType\", \"Homepage\"):\n\t\treturn\n\tif not frappe.db.exists(\"Custom Field\", {\"fieldname\": \"products\", \"dt\": \"Homepage\"}):\n\t\tcustom_fields = {\n\t\t\t\"Homepage\": [\n\t\t\t\tdict(\n\t\t\t\t\tfieldname=\"products_section_break\",\n\t\t\t\t\tlabel=\"Products\",\n\t\t\t\t\tfieldtype=\"Section Break\",\n\t\t\t\t\tinsert_after=\"hero_section\",\n\t\t\t\t),\n\t\t\t\tdict(\n\t\t\t\t\tfieldname=\"products_url\",\n\t\t\t\t\tlabel=\"URL for All Products\",\n\t\t\t\t\tfieldtype=\"Data\",\n\t\t\t\t\tinsert_after=\"products_section_break\",\n\t\t\t\t),\n\t\t\t\tdict(\n\t\t\t\t\tfieldname=\"products\",\n\t\t\t\t\tlabel=\"Products\",\n\t\t\t\t\tfieldtype=\"Table\",\n\t\t\t\t\tinsert_after=\"products_url\",\n\t\t\t\t\toptions=\"Homepage Featured Product\",\n\t\t\t\t),\n\t\t\t],\n\t\t}\n\n\t\tcreate_custom_fields(custom_fields)\n"
  },
  {
    "path": "webshop/patches/clear_cache_for_item_group_route.py",
    "content": "import frappe\nfrom frappe.website.utils import clear_cache\n\ndef execute():\n\troutes = frappe.get_all(\"Item Group\", filters={\"show_in_website\": 1, \"route\": (\"is\", \"set\")}, pluck=\"route\")\n\tfor route in routes:\n\t\tclear_cache(route)"
  },
  {
    "path": "webshop/patches/convert_to_website_item_in_item_card_group_template.py",
    "content": "import json\nfrom typing import List, Union\n\nimport frappe\n\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\n\n\ndef execute():\n\t\"\"\"\n\tConvert all Item links to Website Item link values in\n\texisitng 'Item Card Group' Web Page Block data.\n\t\"\"\"\n\tfrappe.reload_doc(\"webshop\", \"web_template\", \"item_card_group\")\n\n\tblocks = frappe.db.get_all(\n\t\t\"Web Page Block\",\n\t\tfilters={\"web_template\": \"Item Card Group\"},\n\t\tfields=[\"parent\", \"web_template_values\", \"name\"],\n\t)\n\n\tfields = generate_fields_to_edit()\n\n\tfor block in blocks:\n\t\tweb_template_value = json.loads(block.get(\"web_template_values\"))\n\n\t\tfor field in fields:\n\t\t\titem = web_template_value.get(field)\n\t\t\tif not item:\n\t\t\t\tcontinue\n\n\t\t\tif frappe.db.exists(\"Website Item\", {\"item_code\": item}):\n\t\t\t\twebsite_item = frappe.db.get_value(\"Website Item\", {\"item_code\": item})\n\t\t\telse:\n\t\t\t\twebsite_item = make_new_website_item(item)\n\n\t\t\tif website_item:\n\t\t\t\tweb_template_value[field] = website_item\n\n\t\tfrappe.db.set_value(\n\t\t\t\"Web Page Block\", block.name, \"web_template_values\", json.dumps(web_template_value)\n\t\t)\n\n\ndef generate_fields_to_edit() -> List:\n\tfields = []\n\tfor i in range(1, 13):\n\t\tfields.append(f\"card_{i}_item\")  # fields like 'card_1_item', etc.\n\n\treturn fields\n\n\ndef make_new_website_item(item: str) -> Union[str, None]:\n\ttry:\n\t\tdoc = frappe.get_doc(\"Item\", item)\n\t\tweb_item = make_website_item(doc)  # returns [website_item.name, item_name]\n\t\treturn web_item[0]\n\texcept Exception:\n\t\tdoc.log_error(\"Website Item creation failed\")\n\t\treturn None"
  },
  {
    "path": "webshop/patches/copy_custom_field_filters_to_website_item.py",
    "content": "import frappe\nfrom frappe.custom.doctype.custom_field.custom_field import create_custom_field\n\nfrom webshop.webshop.utils.setup import has_ecommerce_fields\n\ndef execute():\n\t\"Add Field Filters, that are not standard fields in Website Item, as Custom Fields.\"\n\n\tdef move_table_multiselect_data(docfield):\n\t\t\"Copy child table data (Table Multiselect) from Item to Website Item for a docfield.\"\n\t\ttable_multiselect_data = get_table_multiselect_data(docfield)\n\t\tfield = docfield.fieldname\n\n\t\tfor row in table_multiselect_data:\n\t\t\t# add copied multiselect data rows in Website Item\n\t\t\tweb_item = frappe.db.get_value(\"Website Item\", {\"item_code\": row.parent})\n\t\t\tweb_item_doc = frappe.get_doc(\"Website Item\", web_item)\n\n\t\t\tchild_doc = frappe.new_doc(docfield.options, parent_doc=web_item_doc, parentfield=field)\n\n\t\t\tfor field in [\"name\", \"creation\", \"modified\", \"idx\"]:\n\t\t\t\trow[field] = None\n\n\t\t\tchild_doc.update(row)\n\n\t\t\tchild_doc.parenttype = \"Website Item\"\n\t\t\tchild_doc.parent = web_item\n\n\t\t\tchild_doc.insert()\n\n\tdef get_table_multiselect_data(docfield):\n\t\tchild_table = frappe.qb.DocType(docfield.options)\n\t\titem = frappe.qb.DocType(\"Item\")\n\n\t\ttable_multiselect_data = (  # query table data for field\n\t\t\tfrappe.qb.from_(child_table)\n\t\t\t.join(item)\n\t\t\t.on(item.item_code == child_table.parent)\n\t\t\t.select(child_table.star)\n\t\t\t.where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))\n\t\t).run(as_dict=True)\n\n\t\treturn table_multiselect_data\n\n\tsettings_doctype = \"E Commerce Settings\" if has_ecommerce_fields() else \"Webshop Settings\"\n\n\tsettings = frappe.get_doc(settings_doctype)\n\n\tif not (settings.enable_field_filters or settings.filter_fields):\n\t\treturn\n\n\titem_meta = frappe.get_meta(\"Item\")\n\tvalid_item_fields = [\n\t\tdf.fieldname for df in item_meta.fields if df.fieldtype in [\"Link\", \"Table MultiSelect\"]\n\t]\n\n\tweb_item_meta = frappe.get_meta(\"Website Item\")\n\tvalid_web_item_fields = [\n\t\tdf.fieldname for df in web_item_meta.fields if df.fieldtype in [\"Link\", \"Table MultiSelect\"]\n\t]\n\n\tfor row in settings.filter_fields:\n\t\t# skip if illegal field\n\t\tif row.fieldname not in valid_item_fields:\n\t\t\tcontinue\n\n\t\t# if Item field is not in Website Item, add it as a custom field\n\t\tif row.fieldname not in valid_web_item_fields:\n\t\t\tdf = item_meta.get_field(row.fieldname)\n\t\t\tcreate_custom_field(\n\t\t\t\t\"Website Item\",\n\t\t\t\tdict(\n\t\t\t\t\towner=\"Administrator\",\n\t\t\t\t\tfieldname=df.fieldname,\n\t\t\t\t\tlabel=df.label,\n\t\t\t\t\tfieldtype=df.fieldtype,\n\t\t\t\t\toptions=df.options,\n\t\t\t\t\tdescription=df.description,\n\t\t\t\t\tread_only=df.read_only,\n\t\t\t\t\tno_copy=df.no_copy,\n\t\t\t\t\tinsert_after=\"on_backorder\",\n\t\t\t\t),\n\t\t\t)\n\n\t\t\t# map field values\n\t\t\tif df.fieldtype == \"Table MultiSelect\":\n\t\t\t\tmove_table_multiselect_data(df)\n\t\t\telse:\n\t\t\t\tfrappe.db.sql(  # nosemgrep\n\t\t\t\t\t\"\"\"\n\t\t\t\t\t\tUPDATE `tabWebsite Item` wi, `tabItem` i\n\t\t\t\t\t\tSET wi.{0} = i.{0}\n\t\t\t\t\t\tWHERE wi.item_code = i.item_code\n\t\t\t\t\t\"\"\".format(\n\t\t\t\t\t\trow.fieldname\n\t\t\t\t\t)\n\t\t\t\t)"
  },
  {
    "path": "webshop/patches/create_website_items.py",
    "content": "import frappe\n\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\n\n\ndef execute():\n\tif frappe.get_all(\"Website Item\", limit=1):\n\t\treturn\n\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"website_item\")\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"website_item_tabbed_section\")\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"website_offer\")\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"recommended_items\")\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"webshop_settings\")\n\tfrappe.reload_doc(\"stock\", \"doctype\", \"item\")\n\n\titem_fields = [\n\t\t\"item_code\",\n\t\t\"item_name\",\n\t\t\"item_group\",\n\t\t\"stock_uom\",\n\t\t\"brand\",\n\t\t\"has_variants\",\n\t\t\"variant_of\",\n\t\t\"description\",\n\t\t\"weightage\",\n\t]\n\tweb_fields_to_map = [\n\t\t\"route\",\n\t\t\"slideshow\",\n\t\t\"website_image_alt\",\n\t\t\"website_warehouse\",\n\t\t\"web_long_description\",\n\t\t\"website_content\",\n\t\t\"website_image\",\n\t\t\"thumbnail\",\n\t]\n\n\t# get all valid columns (fields) from Item master DB schema\n\titem_table_fields = frappe.db.sql(\"desc `tabItem`\", as_dict=1)  # nosemgrep\n\titem_table_fields = [d.get(\"Field\") for d in item_table_fields]\n\n\t# prepare fields to query from Item, check if the web field exists in Item master\n\tweb_query_fields = []\n\tfor web_field in web_fields_to_map:\n\t\tif web_field in item_table_fields:\n\t\t\tweb_query_fields.append(web_field)\n\t\t\titem_fields.append(web_field)\n\n\t# check if the filter fields exist in Item master\n\tor_filters = {}\n\tfor field in [\"show_in_website\", \"show_variant_in_website\"]:\n\t\tif field in item_table_fields:\n\t\t\tor_filters[field] = 1\n\n\tif not web_query_fields or not or_filters:\n\t\t# web fields to map are not present in Item master schema\n\t\t# most likely a fresh installation that doesnt need this patch\n\t\treturn\n\n\titems = frappe.db.get_all(\"Item\", fields=item_fields, or_filters=or_filters)\n\ttotal_count = len(items)\n\n\tfor count, item in enumerate(items, start=1):\n\t\tif frappe.db.exists(\"Website Item\", {\"item_code\": item.item_code}):\n\t\t\tcontinue\n\n\t\t# make new website item from item (publish item)\n\t\twebsite_item = make_website_item(item, save=False)\n\t\twebsite_item.ranking = item.get(\"weightage\")\n\n\t\tfor field in web_fields_to_map:\n\t\t\twebsite_item.update({field: item.get(field)})\n\n\t\twebsite_item.save()\n\n\t\t# move Website Item Group & Website Specification table to Website Item\n\t\tfor doctype in (\"Website Item Group\", \"Item Website Specification\"):\n\t\t\tfrappe.db.set_value(\n\t\t\t\tdoctype,\n\t\t\t\t{\"parenttype\": \"Item\", \"parent\": item.item_code},  # filters\n\t\t\t\t{\"parenttype\": \"Website Item\", \"parent\": website_item.name},  # value dict\n\t\t\t)\n\n\t\tif count % 20 == 0:  # commit after every 20 items\n\t\t\tfrappe.db.commit()\n\n\t\tfrappe.utils.update_progress_bar(\"Creating Website Items\", count, total_count)\n"
  },
  {
    "path": "webshop/patches/enable_allow_to_guest_view_for_item_group.py",
    "content": "import frappe\nfrom frappe.custom.doctype.property_setter.property_setter import make_property_setter\n\ndef execute():\n\tfrappe.reload_doc(\"setup\", \"doctype\", \"item_group\")\n\n\tmake_property_setter(\"Item Group\", \"\", \"has_web_view\", 1, \"Check\", for_doctype=True, validate_fields_for_doctype=False)\n\tmake_property_setter(\"Item Group\", \"\", \"allow_guest_to_view\", 1, \"Check\", for_doctype=True, validate_fields_for_doctype=False)\n"
  },
  {
    "path": "webshop/patches/fetch_thumbnail_in_website_items.py",
    "content": "import frappe\n\n\ndef execute():\n\tif frappe.db.has_column(\"Item\", \"thumbnail\"):\n\t\twebsite_item = frappe.qb.DocType(\"Website Item\").as_(\"wi\")\n\t\titem = frappe.qb.DocType(\"Item\")\n\n\t\tfrappe.qb.update(website_item).inner_join(item).on(website_item.item_code == item.item_code).set(\n\t\t\twebsite_item.thumbnail, item.thumbnail\n\t\t).where(website_item.website_image.notnull() & website_item.thumbnail.isnull()).run()"
  },
  {
    "path": "webshop/patches/make_homepage_products_website_items.py",
    "content": "import frappe\n\n\ndef execute():\n\tif not frappe.db.exists(\"DocType\", \"Homepage\"):\n\t\treturn\n\thomepage = frappe.get_doc(\"Homepage\")\n\n\tfor row in homepage.products:\n\t\tweb_item = frappe.db.get_value(\"Website Item\", {\"item_code\": row.item_code}, \"name\")\n\t\tif not web_item:\n\t\t\tcontinue\n\n\t\trow.item_code = web_item\n\n\thomepage.flags.ignore_mandatory = True\n\thomepage.save()"
  },
  {
    "path": "webshop/patches/populate_e_commerce_settings.py",
    "content": "import frappe\nfrom frappe.utils import cint\n\nfrom webshop.webshop.utils.setup import has_ecommerce_fields\n\ndef execute():\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"webshop_settings\")\n\tfrappe.reload_doc(\"portal\", \"doctype\", \"website_filter_field\")\n\tfrappe.reload_doc(\"portal\", \"doctype\", \"website_attribute\")\n\n\tproducts_settings_fields = [\n\t\t\"hide_variants\",\n\t\t\"products_per_page\",\n\t\t\"enable_attribute_filters\",\n\t\t\"enable_field_filters\",\n\t]\n\n\tshopping_cart_settings_fields = [\n\t\t\"enabled\",\n\t\t\"show_attachments\",\n\t\t\"show_price\",\n\t\t\"show_stock_availability\",\n\t\t\"enable_variants\",\n\t\t\"show_contact_us_button\",\n\t\t\"show_quantity_in_website\",\n\t\t\"show_apply_coupon_code_in_website\",\n\t\t\"allow_items_not_in_stock\",\n\t\t\"company\",\n\t\t\"price_list\",\n\t\t\"default_customer_group\",\n\t\t\"quotation_series\",\n\t\t\"enable_checkout\",\n\t\t\"payment_success_url\",\n\t\t\"payment_gateway_account\",\n\t\t\"save_quotations_as_draft\",\n\t]\n\n\tsettings_doctype = \"E Commerce Settings\" if has_ecommerce_fields() else \"Webshop Settings\"\n\n\tsettings = frappe.get_doc(settings_doctype)\n\n\tdef map_into_e_commerce_settings(doctype, fields):\n\t\tsingles = frappe.qb.DocType(\"Singles\")\n\t\tquery = (\n\t\t\tfrappe.qb.from_(singles)\n\t\t\t.select(singles[\"field\"], singles.value)\n\t\t\t.where((singles.doctype == doctype) & (singles[\"field\"].isin(fields)))\n\t\t)\n\t\tdata = query.run(as_dict=True)\n\n\t\t# {'enable_attribute_filters': '1', ...}\n\t\tmapper = {row.field: row.value for row in data}\n\n\t\tfor key, value in mapper.items():\n\t\t\tvalue = cint(value) if (value and value.isdigit()) else value\n\t\t\tsettings.update({key: value})\n\n\t\tsettings.save()\n\n\t# shift data to E Commerce Settings\n\tmap_into_e_commerce_settings(\"Products Settings\", products_settings_fields)\n\tmap_into_e_commerce_settings(\"Shopping Cart Settings\", shopping_cart_settings_fields)\n\n\t# move filters and attributes tables to E Commerce Settings from Products Settings\n\tfor doctype in (\"Website Filter Field\", \"Website Attribute\"):\n\t\tfrappe.db.set_value(\n\t\t\tdoctype,\n\t\t\t{\"parent\": \"Products Settings\"},\n\t\t\t{\"parenttype\": settings_doctype, \"parent\": settings_doctype},\n\t\t\tupdate_modified=False,\n\t\t)"
  },
  {
    "path": "webshop/patches/shopping_cart_to_ecommerce.py",
    "content": "import click\nimport frappe\n\n\ndef execute():\n\n\tfrappe.delete_doc(\"DocType\", \"Shopping Cart Settings\", ignore_missing=True)\n\tfrappe.delete_doc(\"DocType\", \"Products Settings\", ignore_missing=True)\n\tfrappe.delete_doc(\"DocType\", \"Supplier Item Group\", ignore_missing=True)\n"
  },
  {
    "path": "webshop/patches.txt",
    "content": "[pre_model_sync]\n\n[post_model_sync]\n\nwebshop.patches.add_homepage_field #09-05-2024\nwebshop.patches.enable_allow_to_guest_view_for_item_group\nwebshop.patches.clear_cache_for_item_group_route"
  },
  {
    "path": "webshop/public/.gitkeep",
    "content": ""
  },
  {
    "path": "webshop/public/js/customer_reviews.js",
    "content": "$(() => {\n\tclass CustomerReviews {\n\t\tconstructor() {\n\t\t\tthis.bind_button_actions();\n\t\t\tthis.start = 0;\n\t\t\tthis.page_length = 10;\n\t\t}\n\n\t\tbind_button_actions() {\n\t\t\tthis.write_review();\n\t\t\tthis.view_more();\n\t\t}\n\n\t\twrite_review() {\n\t\t\t//TODO: make dialog popup on stray page\n\t\t\t$('.page_content').on('click', '.btn-write-review', (e) => {\n\t\t\t\t// Bind action on write a review button\n\t\t\t\tconst $btn = $(e.currentTarget);\n\n\t\t\t\tlet d = new frappe.ui.Dialog({\n\t\t\t\t\ttitle: __(\"Write a Review\"),\n\t\t\t\t\tfields: [\n\t\t\t\t\t\t{fieldname: \"title\", fieldtype: \"Data\", label: \"Headline\", reqd: 1},\n\t\t\t\t\t\t{fieldname: \"rating\", fieldtype: \"Rating\", label: \"Overall Rating\", reqd: 1},\n\t\t\t\t\t\t{fieldtype: \"Section Break\"},\n\t\t\t\t\t\t{fieldname: \"comment\", fieldtype: \"Small Text\", label: \"Your Review\"}\n\t\t\t\t\t],\n\t\t\t\t\tprimary_action: function() {\n\t\t\t\t\t\tlet data = d.get_values();\n\t\t\t\t\t\tfrappe.call({\n\t\t\t\t\t\t\tmethod: \"webshop.webshop.doctype.item_review.item_review.add_item_review\",\n\t\t\t\t\t\t\targs: {\n\t\t\t\t\t\t\t\tweb_item: $btn.attr('data-web-item'),\n\t\t\t\t\t\t\t\ttitle: data.title,\n\t\t\t\t\t\t\t\trating: data.rating,\n\t\t\t\t\t\t\t\tcomment: data.comment\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tfreeze: true,\n\t\t\t\t\t\t\tfreeze_message: __(\"Submitting Review ...\"),\n\t\t\t\t\t\t\tcallback: (r) => {\n\t\t\t\t\t\t\t\tif (!r.exc) {\n\t\t\t\t\t\t\t\t\tfrappe.msgprint({\n\t\t\t\t\t\t\t\t\t\tmessage: __(\"Thank you for submitting your review\"),\n\t\t\t\t\t\t\t\t\t\ttitle: __(\"Review Submitted\"),\n\t\t\t\t\t\t\t\t\t\tindicator: \"green\"\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\td.hide();\n\t\t\t\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tprimary_action_label: __(\"Submit\")\n\t\t\t\t});\n\t\t\t\td.show();\n\t\t\t});\n\t\t}\n\n\t\tview_more() {\n\t\t\t$('.page_content').on('click', '.btn-view-more', (e) => {\n\t\t\t\t// Bind action on view more button\n\t\t\t\tconst $btn = $(e.currentTarget);\n\t\t\t\t$btn.prop('disabled', true);\n\n\t\t\t\tthis.start += this.page_length;\n\t\t\t\tlet me = this;\n\n\t\t\t\tfrappe.call({\n\t\t\t\t\tmethod: \"webshop.webshop.doctype.item_review.item_review.get_item_reviews\",\n\t\t\t\t\targs: {\n\t\t\t\t\t\tweb_item: $btn.attr('data-web-item'),\n\t\t\t\t\t\tstart: me.start,\n\t\t\t\t\t\tend: me.page_length\n\t\t\t\t\t},\n\t\t\t\t\tcallback: (result) => {\n\t\t\t\t\t\tif (result.message) {\n\t\t\t\t\t\t\tlet res = result.message;\n\t\t\t\t\t\t\tme.get_user_review_html(res.reviews);\n\n\t\t\t\t\t\t\t$btn.prop('disabled', false);\n\t\t\t\t\t\t\tif (res.total_reviews <= (me.start + me.page_length)) {\n\t\t\t\t\t\t\t\t$btn.hide();\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\n\t\t}\n\n\t\tget_user_review_html(reviews) {\n\t\t\tlet me = this;\n\t\t\tlet $content = $('.user-reviews');\n\n\t\t\treviews.forEach((review) => {\n\t\t\t\t$content.append(`\n\t\t\t\t\t<div class=\"mb-3 review\">\n\t\t\t\t\t\t<div class=\"d-flex\">\n\t\t\t\t\t\t\t<p class=\"mr-4 user-review-title\">\n\t\t\t\t\t\t\t\t<span>${__(review.review_title)}</span>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<div class=\"rating\">\n\t\t\t\t\t\t\t\t${me.get_review_stars(review.rating)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"product-description mb-4\">\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t${__(review.comment)}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"review-signature mb-2\">\n\t\t\t\t\t\t\t<span class=\"reviewer\">${__(review.customer)}</span>\n\t\t\t\t\t\t\t<span class=\"indicator grey\" style=\"--text-on-gray: var(--gray-300);\"></span>\n\t\t\t\t\t\t\t<span class=\"reviewer\">${__(review.published_on)}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t`);\n\t\t\t});\n\t\t}\n\n\t\tget_review_stars(rating) {\n\t\t\tlet stars = ``;\n\t\t\tfor (let i = 1; i < 6; i++) {\n\t\t\t\tlet fill_class = i <= rating ? 'star-click' : '';\n\t\t\t\tstars += `\n\t\t\t\t\t<svg class=\"icon icon-sm ${fill_class}\">\n\t\t\t\t\t\t<use href=\"#icon-star\"></use>\n\t\t\t\t\t</svg>\n\t\t\t\t`;\n\t\t\t}\n\t\t\treturn stars;\n\t\t}\n\t}\n\n\tnew CustomerReviews();\n});\n"
  },
  {
    "path": "webshop/public/js/init.js",
    "content": "if (!window.webshop) window.webshop = {}\nif (!frappe.boot) frappe.boot = {}\n"
  },
  {
    "path": "webshop/public/js/override/homepage.js",
    "content": "frappe.ui.form.on('Homepage', {\n\tsetup: function(frm) {\n\t\tfrm.set_query('item_code', 'products', function() {\n\t\t\treturn {\n\t\t\t\tfilters: {'published': 1}\n\t\t\t};\n\t\t});\n\t},\n});\n\nfrappe.ui.form.on('Homepage Featured Product', {\n\tview: function(frm, cdt, cdn) {\n\t\tvar child= locals[cdt][cdn];\n\t\tif (child.item_code && child.route) {\n\t\t\twindow.open('/' + child.route, '_blank');\n\t\t}\n\t}\n});\n"
  },
  {
    "path": "webshop/public/js/override/item.js",
    "content": "frappe.ui.form.on(\"Item\", {\n    refresh: function(frm) {\n\t\tif (!frm.doc.__islocal) {\n\t\t\tif (!frm.doc.published_in_website) {\n\t\t\t\tfrm.add_custom_button(__(\"Publish in Website\"), function() {\n\t\t\t\t\tfrappe.call({\n\t\t\t\t\t\tmethod: \"webshop.webshop.doctype.website_item.website_item.make_website_item\",\n\t\t\t\t\t\targs: {\n\t\t\t\t\t\t\tdoc: frm.doc,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfreeze: true,\n\t\t\t\t\t\tfreeze_message: __(\"Publishing Item ...\"),\n\t\t\t\t\t\tcallback: function(result) {\n\t\t\t\t\t\t\tfrappe.msgprint({\n\t\t\t\t\t\t\t\tmessage: __(\"Website Item {0} has been created.\",\n\t\t\t\t\t\t\t\t\t[repl('<a href=\"/app/website-item/%(item_encoded)s\" class=\"strong\">%(item)s</a>', {\n\t\t\t\t\t\t\t\t\t\titem_encoded: encodeURIComponent(result.message[0]),\n\t\t\t\t\t\t\t\t\t\titem: result.message[1]\n\t\t\t\t\t\t\t\t\t})]\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\ttitle: __(\"Published\"),\n\t\t\t\t\t\t\t\tindicator: \"green\"\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}, __('Actions'));\n\t\t\t} else {\n\t\t\t\tfrm.add_custom_button(__(\"View Website Item\"), function() {\n\t\t\t\t\tfrappe.db.get_value(\"Website Item\", {item_code: frm.doc.name}, \"name\", (d) => {\n\t\t\t\t\t\tif (!d.name) frappe.throw(__(\"Website Item not found\"));\n\t\t\t\t\t\tfrappe.set_route(\"Form\", \"Website Item\", d.name);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n});\n"
  },
  {
    "path": "webshop/public/js/product_ui/grid.js",
    "content": "webshop.ProductGrid = class {\n\t/* Options:\n\t\t- items: Items\n\t\t- settings: Webshop Settings\n\t\t- products_section: Products Wrapper\n\t\t- preference: If preference is not grid view, render but hide\n\t*/\n\tconstructor(options) {\n\t\tObject.assign(this, options);\n\n\t\tif (this.preference !== \"Grid View\") {\n\t\t\tthis.products_section.addClass(\"hidden\");\n\t\t}\n\n\t\tthis.products_section.empty();\n\t\tthis.make();\n\t}\n\n\tmake() {\n\t\tlet me = this;\n\t\tlet html = ``;\n\n\t\tthis.items.forEach(item => {\n\t\t\tlet title = item.web_item_name || item.item_name || item.item_code || \"\";\n\t\t\ttitle =  title.length > 90 ? title.substr(0, 90) + \"...\" : title;\n\n\t\t\thtml += `<div class=\"col-sm-4 item-card\"><div class=\"card text-left\">`;\n\t\t\thtml += me.get_image_html(item, title);\n\t\t\thtml += me.get_card_body_html(item, title, me.settings);\n\t\t\thtml += `</div></div>`;\n\t\t});\n\n\t\tlet $product_wrapper = this.products_section;\n\t\t$product_wrapper.append(html);\n\t}\n\n\tget_image_html(item, title) {\n\t\tlet image = item.website_image;\n\n\t\tif (image) {\n\t\t\treturn `\n\t\t\t\t<div class=\"card-img-container\">\n\t\t\t\t\t<a href=\"/${ item.route || '#' }\" style=\"text-decoration: none;\">\n\t\t\t\t\t\t<img itemprop=\"image\" class=\"card-img\" src=\"${ image }\" alt=\"${ title }\">\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t`;\n\t\t} else {\n\t\t\treturn `\n\t\t\t\t<div class=\"card-img-container\">\n\t\t\t\t\t<a href=\"/${ item.route || '#' }\" style=\"text-decoration: none;\">\n\t\t\t\t\t\t<div class=\"card-img-top no-image\">\n\t\t\t\t\t\t\t${ frappe.get_abbr(title) }\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t`;\n\t\t}\n\t}\n\n\tget_card_body_html(item, title, settings) {\n\t\tlet body_html = `\n\t\t\t<div class=\"card-body text-left card-body-flex\" style=\"width:100%\">\n\t\t\t\t<div style=\"margin-top: 1rem; display: flex;\">\n\t\t`;\n\t\tbody_html += this.get_title(item, title);\n\n\t\t// get floating elements\n\t\tif (!item.has_variants) {\n\t\t\tif (settings.enable_wishlist) {\n\t\t\t\tbody_html += this.get_wishlist_icon(item);\n\t\t\t}\n\t\t\tif (settings.enabled) {\n\t\t\t\tbody_html += this.get_cart_indicator(item);\n\t\t\t}\n\n\t\t}\n\n\t\tbody_html += `</div>`;\n\t\tbody_html += `<div class=\"product-category\" itemprop=\"name\">${ item.item_group || '' }</div>`;\n\n\t\tif (item.formatted_price) {\n\t\t\tbody_html += this.get_price_html(item);\n\t\t}\n\n\t\tbody_html += this.get_stock_availability(item, settings);\n\t\tbody_html += this.get_primary_button(item, settings);\n\t\tbody_html += `</div>`; // close div on line 49\n\n\t\treturn body_html;\n\t}\n\n\tget_title(item, title) {\n\t\tlet title_html = `\n\t\t\t<a href=\"/${ item.route || '#' }\">\n\t\t\t\t<div class=\"product-title\" itemprop=\"name\">\n\t\t\t\t\t${ title || '' }\n\t\t\t\t</div>\n\t\t\t</a>\n\t\t`;\n\t\treturn title_html;\n\t}\n\n\tget_wishlist_icon(item) {\n\t\tlet icon_class = item.wished ? \"wished\" : \"not-wished\";\n\t\treturn `\n\t\t\t<div class=\"like-action ${ item.wished ? \"like-action-wished\" : ''}\"\n\t\t\t\tdata-item-code=\"${ item.item_code }\">\n\t\t\t\t<svg class=\"icon sm\">\n\t\t\t\t\t<use class=\"${ icon_class } wish-icon\" href=\"#icon-heart\"></use>\n\t\t\t\t</svg>\n\t\t\t</div>\n\t\t`;\n\t}\n\n\tget_cart_indicator(item) {\n\t\treturn `\n\t\t\t<div class=\"cart-indicator ${item.in_cart ? '' : 'hidden'}\" data-item-code=\"${ item.item_code }\">\n\t\t\t\t1\n\t\t\t</div>\n\t\t`;\n\t}\n\n\tget_price_html(item) {\n\t\tlet price_html = `\n\t\t\t<div class=\"product-price\" itemprop=\"offers\" itemscope itemtype=\"https://schema.org/AggregateOffer\">\n\t\t\t\t${ item.formatted_price || '' }\n\t\t`;\n\n\t\tif (item.formatted_mrp) {\n\t\t\tprice_html += `\n\t\t\t\t<small class=\"striked-price\">\n\t\t\t\t\t<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, \"\") : \"\" }</s>\n\t\t\t\t</small>\n\t\t\t\t<small class=\"ml-1 product-info-green\">\n\t\t\t\t\t${ item.discount } ${ __(\"OFF\") }\n\t\t\t\t</small>\n\t\t\t`;\n\t\t}\n\t\tprice_html += `</div>`;\n\t\treturn price_html;\n\t}\n\n\tget_stock_availability(item, settings) {\n\t\tif (settings.show_stock_availability && !item.has_variants) {\n\t\t\tif (item.on_backorder) {\n\t\t\t\treturn `\n\t\t\t\t\t<span class=\"out-of-stock mb-2 mt-1\" style=\"color: var(--primary-color)\">\n\t\t\t\t\t\t${ __(\"Available on backorder\") }\n\t\t\t\t\t</span>\n\t\t\t\t`;\n\t\t\t} else if (!item.in_stock) {\n\t\t\t\treturn `\n\t\t\t\t\t<span class=\"out-of-stock mb-2 mt-1\">\n\t\t\t\t\t\t${ __(\"Out of stock\") }\n\t\t\t\t\t</span>\n\t\t\t\t`;\n\t\t\t}\n\t\t}\n\n\t\treturn ``;\n\t}\n\n\tget_primary_button(item, settings) {\n\t\tif (item.has_variants) {\n\t\t\treturn `\n\t\t\t\t<a href=\"/${ item.route || '#' }\">\n\t\t\t\t\t<div class=\"btn btn-sm btn-explore-variants w-100 mt-4\">\n\t\t\t\t\t\t${ __(\"Explore\") }\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t`;\n\t\t} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {\n\t\t\treturn `\n\t\t\t\t<div id=\"${ item.name }\" class=\"btn\n\t\t\t\t\tbtn-sm btn-primary btn-add-to-cart-list\n\t\t\t\t\tw-100 mt-2 ${ item.in_cart ? 'hidden' : '' }\"\n\t\t\t\t\tdata-item-code=\"${ item.item_code }\">\n\t\t\t\t\t<span class=\"mr-2\">\n\t\t\t\t\t\t<svg class=\"icon icon-md\">\n\t\t\t\t\t\t\t<use href=\"#icon-assets\"></use>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t\t${ settings.enable_checkout ? __(\"Add to Cart\") :  __(\"Add to Quote\") }\n\t\t\t\t</div>\n\n\t\t\t\t<a href=\"/cart\">\n\t\t\t\t\t<div id=\"${ item.name }\" class=\"btn\n\t\t\t\t\t\tbtn-sm btn-primary btn-add-to-cart-list\n\t\t\t\t\t\tw-100 mt-4 go-to-cart-grid\n\t\t\t\t\t\t${ item.in_cart ? '' : 'hidden' }\"\n\t\t\t\t\t\tdata-item-code=\"${ item.item_code }\">\n\t\t\t\t\t\t${ settings.enable_checkout ? __(\"Go to Cart\") :  __(\"Go to Quote\") }\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t`;\n\t\t} else {\n\t\t\treturn ``;\n\t\t}\n\t}\n};\n"
  },
  {
    "path": "webshop/public/js/product_ui/list.js",
    "content": "webshop.ProductList = class {\n\t/* Options:\n\t\t- items: Items\n\t\t- settings: Webshop Settings\n\t\t- products_section: Products Wrapper\n\t\t- preference: If preference is not list view, render but hide\n\t*/\n\tconstructor(options) {\n\t\tObject.assign(this, options);\n\n\t\tif (this.preference !== \"List View\") {\n\t\t\tthis.products_section.addClass(\"hidden\");\n\t\t}\n\n\t\tthis.products_section.empty();\n\t\tthis.make();\n\t}\n\n\tmake() {\n\t\tlet me = this;\n\t\tlet html = `<br><br>`;\n\n\t\tthis.items.forEach(item => {\n\t\t\tlet title = item.web_item_name || item.item_name || item.item_code || \"\";\n\t\t\ttitle =  title.length > 200 ? title.substr(0, 200) + \"...\" : title;\n\n\t\t\thtml += `<div class='row list-row w-100 mb-4'>`;\n\t\t\thtml += me.get_image_html(item, title, me.settings);\n\t\t\thtml += me.get_row_body_html(item, title, me.settings);\n\t\t\thtml += `</div>`;\n\t\t});\n\n\t\tlet $product_wrapper = this.products_section;\n\t\t$product_wrapper.append(html);\n\t}\n\n\tget_image_html(item, title, settings) {\n\t\tlet image = item.website_image;\n\t\tlet wishlist_enabled = !item.has_variants && settings.enable_wishlist;\n\t\tlet image_html = ``;\n\n\t\tif (image) {\n\t\t\timage_html += `\n\t\t\t\t<div class=\"col-2 border text-center rounded list-image\">\n\t\t\t\t\t<a class=\"product-link product-list-link\" href=\"/${ item.route || '#' }\">\n\t\t\t\t\t\t<img itemprop=\"image\" class=\"website-image h-100 w-100\" alt=\"${ title }\"\n\t\t\t\t\t\t\tsrc=\"${ image }\">\n\t\t\t\t\t</a>\n\t\t\t\t\t${ wishlist_enabled ? this.get_wishlist_icon(item): '' }\n\t\t\t\t</div>\n\t\t\t`;\n\t\t} else {\n\t\t\timage_html += `\n\t\t\t\t<div class=\"col-2 border text-center rounded list-image\">\n\t\t\t\t\t<a class=\"product-link product-list-link\" href=\"/${ item.route || '#' }\"\n\t\t\t\t\t\tstyle=\"text-decoration: none\">\n\t\t\t\t\t\t<div class=\"card-img-top no-image-list\">\n\t\t\t\t\t\t\t${ frappe.get_abbr(title) }\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</a>\n\t\t\t\t\t${ wishlist_enabled ? this.get_wishlist_icon(item): '' }\n\t\t\t\t</div>\n\t\t\t`;\n\t\t}\n\n\t\treturn image_html;\n\t}\n\n\tget_row_body_html(item, title, settings) {\n\t\tlet body_html = `<div class='col-10 text-left'>`;\n\t\tbody_html += this.get_title_html(item, title, settings);\n\t\tbody_html += this.get_item_details(item, settings);\n\t\tbody_html += `</div>`;\n\t\treturn body_html;\n\t}\n\n\tget_title_html(item, title, settings) {\n\t\tlet title_html = `<div style=\"display: flex; margin-left: -15px;\">`;\n\t\ttitle_html += `\n\t\t\t<div class=\"col-8\" style=\"margin-right: -15px;\">\n\t\t\t\t<a class=\"\" href=\"/${ item.route || '#' }\"\n\t\t\t\t\tstyle=\"color: var(--gray-800); font-weight: 500;\">\n\t\t\t\t\t${ title }\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t`;\n\n\t\tif (settings.enabled) {\n\t\t\ttitle_html += `<div class=\"col-4 cart-action-container ${item.in_cart ? 'd-flex' : ''}\">`;\n\t\t\ttitle_html += this.get_primary_button(item, settings);\n\t\t\ttitle_html += `</div>`;\n\t\t}\n\t\ttitle_html += `</div>`;\n\n\t\treturn title_html;\n\t}\n\n\tget_item_details(item, settings) {\n\t\tlet details = `\n\t\t\t<p class=\"product-code\">\n\t\t\t\t${ item.item_group } | ${ __('Item Code') } : ${ item.item_code }\n\t\t\t</p>\n\t\t\t<div class=\"mt-2\" style=\"color: var(--gray-600) !important; font-size: 13px;\">\n\t\t\t\t${ item.short_description || '' }\n\t\t\t</div>\n\t\t\t<div class=\"product-price\" itemprop=\"offers\" itemscope itemtype=\"https://schema.org/AggregateOffer\">\n\t\t\t\t${ item.formatted_price || '' }\n\t\t`;\n\n\t\tif (item.formatted_mrp) {\n\t\t\tdetails += `\n\t\t\t\t<small class=\"striked-price\">\n\t\t\t\t\t<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, \"\") : \"\" }</s>\n\t\t\t\t</small>\n\t\t\t\t<small class=\"ml-1 product-info-green\">\n\t\t\t\t\t${ item.discount } ${ __(\"OFF\") }\n\t\t\t\t</small>\n\t\t\t`;\n\t\t}\n\n\t\tdetails += this.get_stock_availability(item, settings);\n\t\tdetails += `</div>`;\n\n\t\treturn details;\n\t}\n\n\tget_stock_availability(item, settings) {\n\t\tif (settings.show_stock_availability && !item.has_variants) {\n\t\t\tif (item.on_backorder) {\n\t\t\t\treturn `\n\t\t\t\t\t<br>\n\t\t\t\t\t<span class=\"out-of-stock mt-2\" style=\"color: var(--primary-color)\">\n\t\t\t\t\t\t${ __(\"Available on backorder\") }\n\t\t\t\t\t</span>\n\t\t\t\t`;\n\t\t\t} else if (!item.in_stock) {\n\t\t\t\treturn `\n\t\t\t\t\t<br>\n\t\t\t\t\t<span class=\"out-of-stock mt-2\">${ __(\"Out of stock\") }</span>\n\t\t\t\t`;\n\t\t\t} else if (item.is_stock) {\n\t\t\t\treturn `\n\t\t\t\t\t<br>\n\t\t\t\t\t<span class=\"in-stock in-green has-stock mt-2\"\n\t\t\t\t\t\tstyle=\"font-size: 14px;\">${ __(\"In stock\") }</span>\n\t\t\t\t`;\n\t\t\t}\n\t\t}\n\t\treturn ``;\n\t}\n\n\tget_wishlist_icon(item) {\n\t\tlet icon_class = item.wished ? \"wished\" : \"not-wished\";\n\n\t\treturn `\n\t\t\t<div class=\"like-action-list ${ item.wished ? \"like-action-wished\" : ''}\"\n\t\t\t\tdata-item-code=\"${ item.item_code }\">\n\t\t\t\t<svg class=\"icon sm\">\n\t\t\t\t\t<use class=\"${ icon_class } wish-icon\" href=\"#icon-heart\"></use>\n\t\t\t\t</svg>\n\t\t\t</div>\n\t\t`;\n\t}\n\n\tget_primary_button(item, settings) {\n\t\tif (item.has_variants) {\n\t\t\treturn `\n\t\t\t\t<a href=\"/${ item.route || '#' }\">\n\t\t\t\t\t<div class=\"btn btn-sm btn-explore-variants btn mb-0 mt-0\">\n\t\t\t\t\t\t${ __(\"Explore\") }\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t`;\n\t\t} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {\n\t\t\treturn `\n\t\t\t\t<div id=\"${ item.name }\" class=\"btn\n\t\t\t\t\tbtn-sm btn-primary btn-add-to-cart-list mb-0\n\t\t\t\t\t${ item.in_cart ? 'hidden' : '' }\"\n\t\t\t\t\tdata-item-code=\"${ item.item_code }\"\n\t\t\t\t\tstyle=\"margin-top: 0px !important; max-height: 30px; float: right;\n\t\t\t\t\t\tpadding: 0.25rem 1rem; min-width: 135px;\">\n\t\t\t\t\t<span class=\"mr-2\">\n\t\t\t\t\t\t<svg class=\"icon icon-md\">\n\t\t\t\t\t\t\t<use href=\"#icon-assets\"></use>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t\t${ settings.enable_checkout ? __(\"Add to Cart\") :  __(\"Add to Quote\") }\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"cart-indicator list-indicator ${item.in_cart ? '' : 'hidden'}\">\n\t\t\t\t\t1\n\t\t\t\t</div>\n\n\t\t\t\t<a href=\"/cart\">\n\t\t\t\t\t<div id=\"${ item.name }\" class=\"btn\n\t\t\t\t\t\tbtn-sm btn-primary btn-add-to-cart-list\n\t\t\t\t\t\tml-4 go-to-cart mb-0 mt-0\n\t\t\t\t\t\t${ item.in_cart ? '' : 'hidden' }\"\n\t\t\t\t\t\tdata-item-code=\"${ item.item_code }\"\n\t\t\t\t\t\tstyle=\"padding: 0.25rem 1rem; min-width: 135px;\">\n\t\t\t\t\t\t${ settings.enable_checkout ? __(\"Go to Cart\") :  __(\"Go to Quote\") }\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t`;\n\t\t} else {\n\t\t\treturn ``;\n\t\t}\n\t}\n\n};\n"
  },
  {
    "path": "webshop/public/js/product_ui/search.js",
    "content": "webshop.ProductSearch = class {\n\tconstructor(opts) {\n\t\t/* Options: search_box_id (for custom search box) */\n\t\t$.extend(this, opts);\n\t\tthis.MAX_RECENT_SEARCHES = 4;\n\t\tthis.search_box_id = this.search_box_id || \"#search-box\";\n\t\tthis.searchBox = $(this.search_box_id);\n\n\t\tthis.setupSearchDropDown();\n\t\tthis.bindSearchAction();\n\t}\n\n\tsetupSearchDropDown() {\n\t\tthis.search_area = $(\"#dropdownMenuSearch\");\n\t\tthis.setupSearchResultContainer();\n\t\tthis.populateRecentSearches();\n\t}\n\n\tbindSearchAction() {\n\t\tlet me = this;\n\n\t\t// Show Search dropdown\n\t\tthis.searchBox.on(\"focus\", () => {\n\t\t\tthis.search_dropdown.removeClass(\"hidden\");\n\t\t});\n\n\t\t// If click occurs outside search input/results, hide results.\n\t\t// Click can happen anywhere on the page\n\t\t$(\"body\").on(\"click\", (e) => {\n\t\t\tlet searchEvent = $(e.target).closest(this.search_box_id).length;\n\t\t\tlet resultsEvent = $(e.target).closest('#search-results-container').length;\n\t\t\tlet isResultHidden = this.search_dropdown.hasClass(\"hidden\");\n\n\t\t\tif (!searchEvent && !resultsEvent && !isResultHidden) {\n\t\t\t\tthis.search_dropdown.addClass(\"hidden\");\n\t\t\t}\n\t\t});\n\n\t\t// Process search input\n\t\tthis.searchBox.on(\"input\", (e) => {\n\t\t\tlet query = e.target.value;\n\n\t\t\tif (query.length == 0) {\n\t\t\t\tme.populateResults(null);\n\t\t\t\tme.populateCategoriesList(null);\n\t\t\t}\n\n\t\t\tif (query.length < 3 || !query.length) return;\n\n\t\t\tfrappe.call({\n\t\t\t\tmethod: \"webshop.templates.pages.product_search.search\",\n\t\t\t\targs: {\n\t\t\t\t\tquery: query\n\t\t\t\t},\n\t\t\t\tcallback: (data) => {\n\t\t\t\t\tlet product_results = null, category_results = null;\n\n\t\t\t\t\t// Populate product results\n\t\t\t\t\tproduct_results = data.message ? data.message.product_results : null;\n\t\t\t\t\tme.populateResults(product_results);\n\n\t\t\t\t\t// Populate categories\n\t\t\t\t\tif (me.category_container) {\n\t\t\t\t\t\tcategory_results = data.message ? data.message.category_results : null;\n\t\t\t\t\t\tme.populateCategoriesList(category_results);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Populate recent search chips only on successful queries\n\t\t\t\t\tif (!$.isEmptyObject(product_results) || !$.isEmptyObject(category_results)) {\n\t\t\t\t\t\tme.setRecentSearches(query);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tthis.search_dropdown.removeClass(\"hidden\");\n\t\t});\n\t}\n\n\tsetupSearchResultContainer() {\n\t\tthis.search_dropdown = this.search_area.append(`\n\t\t\t<div class=\"overflow-hidden shadow dropdown-menu w-100 hidden\"\n\t\t\t\tid=\"search-results-container\"\n\t\t\t\taria-labelledby=\"dropdownMenuSearch\"\n\t\t\t\tstyle=\"display: flex; flex-direction: column;\">\n\t\t\t</div>\n\t\t`).find(\"#search-results-container\");\n\n\t\tthis.setupCategoryContainer();\n\t\tthis.setupProductsContainer();\n\t\tthis.setupRecentsContainer();\n\t}\n\n\tsetupProductsContainer() {\n\t\tthis.products_container = this.search_dropdown.append(`\n\t\t\t<div id=\"product-results mt-2\">\n\t\t\t\t<div id=\"product-scroll\" style=\"overflow: scroll; max-height: 300px\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t`).find(\"#product-scroll\");\n\t}\n\n\tsetupCategoryContainer() {\n\t\tthis.category_container = this.search_dropdown.append(`\n\t\t\t<div class=\"category-container mt-2 mb-1\">\n\t\t\t\t<div class=\"category-chips\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t`).find(\".category-chips\");\n\t}\n\n\tsetupRecentsContainer() {\n\t\tlet $recents_section = this.search_dropdown.append(`\n\t\t\t<div class=\"mb-2 mt-2 recent-searches\">\n\t\t\t\t<div>\n\t\t\t\t\t<b>${ __(\"Recent\") }</b>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t`).find(\".recent-searches\");\n\n\t\tthis.recents_container = $recents_section.append(`\n\t\t\t<div id=\"recents\" style=\"padding: .25rem 0 1rem 0;\">\n\t\t\t</div>\n\t\t`).find(\"#recents\");\n\t}\n\n\tgetRecentSearches() {\n\t\treturn JSON.parse(localStorage.getItem(\"recent_searches\") || \"[]\");\n\t}\n\n\tattachEventListenersToChips() {\n\t\tlet me  = this;\n\t\tconst chips = $(\".recent-search\");\n\t\twindow.chips = chips;\n\n\t\tfor (let chip of chips) {\n\t\t\tchip.addEventListener(\"click\", () => {\n\t\t\t\tme.searchBox[0].value = chip.innerText.trim();\n\n\t\t\t\t// Start search with `recent query`\n\t\t\t\tme.searchBox.trigger(\"input\");\n\t\t\t\tme.searchBox.focus();\n\t\t\t});\n\t\t}\n\t}\n\n\tsetRecentSearches(query) {\n\t\tlet recents = this.getRecentSearches();\n\t\tif (recents.length >= this.MAX_RECENT_SEARCHES) {\n\t\t\t// Remove the `first` query\n\t\t\trecents.splice(0, 1);\n\t\t}\n\n\t\tif (recents.indexOf(query) >= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\trecents.push(query);\n\t\tlocalStorage.setItem(\"recent_searches\", JSON.stringify(recents));\n\n\t\tthis.populateRecentSearches();\n\t}\n\n\tpopulateRecentSearches() {\n\t\tlet recents = this.getRecentSearches();\n\n\t\tif (!recents.length) {\n\t\t\tthis.recents_container.html(`<span class=\"\"text-muted\">${ __(\"No searches yet.\") }</span>`);\n\t\t\treturn;\n\t\t}\n\n\t\tlet html = \"\";\n\t\trecents.forEach((key) => {\n\t\t\thtml += `\n\t\t\t\t<div class=\"recent-search mr-1\" style=\"font-size: 13px\">\n\t\t\t\t\t<span class=\"mr-2\">\n\t\t\t\t\t\t<svg width=\"20\" height=\"20\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n\t\t\t\t\t\t\t<path d=\"M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z\" stroke=\"var(--gray-500)\"\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t\t<path d=\"M8.00027 5.20947V8.00017L10 10\" stroke=\"var(--gray-500)\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t\t${ key }\n\t\t\t\t</div>\n\t\t\t`;\n\t\t});\n\n\t\tthis.recents_container.html(html);\n\t\tthis.attachEventListenersToChips();\n\t}\n\n\tpopulateResults(product_results) {\n\t\tif (!product_results || product_results.length === 0) {\n\t\t\tlet empty_html = ``;\n\t\t\tthis.products_container.html(empty_html);\n\t\t\treturn;\n\t\t}\n\n\t\tlet html = \"\";\n\n\t\tproduct_results.forEach((res) => {\n\t\t\tlet thumbnail = res.thumbnail || '/assets/webshop/images/cart-empty-state.png';\n\t\t\thtml += `\n\t\t\t\t<div class=\"dropdown-item\" style=\"display: flex;\">\n\t\t\t\t\t<img class=\"item-thumb col-2\" src=${encodeURI(thumbnail)} />\n\t\t\t\t\t<div class=\"col-9\" style=\"white-space: normal;\">\n\t\t\t\t\t\t<a href=\"/${res.route}\">${res.web_item_name}</a><br>\n\t\t\t\t\t\t<span class=\"brand-line\">${res.brand ? \"by \" + res.brand : \"\"}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t`;\n\t\t});\n\n\t\tthis.products_container.html(html);\n\t}\n\n\tpopulateCategoriesList(category_results) {\n\t\tif (!category_results || category_results.length === 0) {\n\t\t\tlet empty_html = `\n\t\t\t\t<div class=\"category-container mt-2\">\n\t\t\t\t\t<div class=\"category-chips\">\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t`;\n\t\t\tthis.category_container.html(empty_html);\n\t\t\treturn;\n\t\t}\n\n\t\tlet html = `\n\t\t\t<div class=\"mb-2\">\n\t\t\t\t<b>${ __(\"Categories\") }</b>\n\t\t\t</div>\n\t\t`;\n\n\t\tcategory_results.forEach((category) => {\n\t\t\thtml += `\n\t\t\t\t<a href=\"/${category.route}\" class=\"btn btn-sm category-chip mr-2 mb-2\"\n\t\t\t\t\tstyle=\"font-size: 13px\" role=\"button\">\n\t\t\t\t${ category.name }\n\t\t\t\t</button>\n\t\t\t`;\n\t\t});\n\n\t\tthis.category_container.html(html);\n\t}\n};\n"
  },
  {
    "path": "webshop/public/js/product_ui/views.js",
    "content": "webshop.ProductView =  class {\n\t/* Options:\n\t\t- View Type\n\t\t- Products Section Wrapper,\n\t\t- Item Group: If its an Item Group page\n\t*/\n\tconstructor(options) {\n\t\tObject.assign(this, options);\n\t\tthis.preference = this.view_type;\n\t\tthis.make();\n\t}\n\n\tmake(from_filters=false) {\n\t\tthis.products_section.empty();\n\t\tthis.prepare_toolbar();\n\t\tthis.get_item_filter_data(from_filters);\n\t}\n\n\tprepare_toolbar() {\n\t\tthis.products_section.append(`\n\t\t\t<div class=\"toolbar d-flex\">\n\t\t\t</div>\n\t\t`);\n\t\tthis.prepare_search();\n\t\tthis.prepare_view_toggler();\n\n\t\tnew webshop.ProductSearch();\n\t}\n\n\tprepare_view_toggler() {\n\n\t\tif (!$(\"#list\").length || !$(\"#image-view\").length) {\n\t\t\tthis.render_view_toggler();\n\t\t\tthis.bind_view_toggler_actions();\n\t\t\tthis.set_view_state();\n\t\t}\n\t}\n\n\tget_item_filter_data(from_filters=false) {\n\t\t// Get and render all Product related views\n\t\tlet me = this;\n\t\tthis.from_filters = from_filters;\n\t\tlet args = this.get_query_filters();\n\n\t\tthis.disable_view_toggler(true);\n\n\t\tfrappe.call({\n\t\t\tmethod: \"webshop.webshop.api.get_product_filter_data\",\n\t\t\targs: {\n\t\t\t\tquery_args: args\n\t\t\t},\n\t\t\tcallback: function(result) {\n\t\t\t\tif (!result || result.exc || !result.message || result.message.exc) {\n\t\t\t\t\tme.render_no_products_section(true);\n\t\t\t\t} else {\n\t\t\t\t\t// Sub Category results are independent of Items\n\t\t\t\t\tif (me.item_group && result.message[\"sub_categories\"].length) {\n\t\t\t\t\t\tme.render_item_sub_categories(result.message[\"sub_categories\"]);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!result.message[\"items\"].length) {\n\t\t\t\t\t\t// if result has no items or result is empty\n\t\t\t\t\t\tme.render_no_products_section();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Add discount filters\n\t\t\t\t\t\tme.re_render_discount_filters(result.message[\"filters\"].discount_filters);\n\n\t\t\t\t\t\t// Render views\n\t\t\t\t\t\tme.render_list_view(result.message[\"items\"], result.message[\"settings\"]);\n\t\t\t\t\t\tme.render_grid_view(result.message[\"items\"], result.message[\"settings\"]);\n\n\t\t\t\t\t\tme.products = result.message[\"items\"];\n\t\t\t\t\t\tme.product_count = result.message[\"items_count\"];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Bind filter actions\n\t\t\t\t\tif (!from_filters) {\n\t\t\t\t\t\t// If `get_product_filter_data` was triggered after checking a filter,\n\t\t\t\t\t\t// don't touch filters unnecessarily, only data must change\n\t\t\t\t\t\t// filter persistence is handle on filter change event\n\t\t\t\t\t\tme.bind_filters();\n\t\t\t\t\t\tme.restore_filters_state();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Bottom paging\n\t\t\t\t\tme.add_paging_section(result.message[\"settings\"]);\n\t\t\t\t}\n\n\t\t\t\tme.disable_view_toggler(false);\n\t\t\t}\n\t\t});\n\t}\n\n\tdisable_view_toggler(disable=false) {\n\t\t$('#list').prop('disabled', disable);\n\t\t$('#image-view').prop('disabled', disable);\n\t}\n\n\trender_grid_view(items, settings) {\n\t\t// loop over data and add grid html to it\n\t\tlet me = this;\n\t\tthis.prepare_product_area_wrapper(\"grid\");\n\n\t\tnew webshop.ProductGrid({\n\t\t\titems: items,\n\t\t\tproducts_section: $(\"#products-grid-area\"),\n\t\t\tsettings: settings,\n\t\t\tpreference: me.preference\n\t\t});\n\t}\n\n\trender_list_view(items, settings) {\n\t\tlet me = this;\n\t\tthis.prepare_product_area_wrapper(\"list\");\n\n\t\tnew webshop.ProductList({\n\t\t\titems: items,\n\t\t\tproducts_section: $(\"#products-list-area\"),\n\t\t\tsettings: settings,\n\t\t\tpreference: me.preference\n\t\t});\n\t}\n\n\tprepare_product_area_wrapper(view) {\n\t\tlet left_margin = view == \"list\" ? \"ml-2\" : \"\";\n\t\tlet top_margin = view == \"list\" ? \"mt-6\" : \"mt-minus-1\";\n\t\treturn this.products_section.append(`\n\t\t\t<br>\n\t\t\t<div id=\"products-${view}-area\" class=\"row products-list ${ top_margin } ${ left_margin }\" itemscope itemtype=\"https://schema.org/Product\"></div>\n\t\t`);\n\t}\n\n\tget_query_filters() {\n\t\tconst filters = frappe.utils.get_query_params();\n\t\tlet {field_filters, attribute_filters} = filters;\n\n\t\tfield_filters = field_filters ? JSON.parse(field_filters) : {};\n\t\tattribute_filters = attribute_filters ? JSON.parse(attribute_filters) : {};\n\n\t\treturn {\n\t\t\tfield_filters: field_filters,\n\t\t\tattribute_filters: attribute_filters,\n\t\t\titem_group: this.item_group,\n\t\t\tstart: filters.start || null,\n\t\t\tfrom_filters: this.from_filters || false\n\t\t};\n\t}\n\n\tadd_paging_section(settings) {\n\t\t$(\".product-paging-area\").remove();\n\n\t\tif (this.products) {\n\t\t\tlet paging_html = `\n\t\t\t\t<div class=\"row product-paging-area mt-5\">\n\t\t\t\t\t<div class=\"col-3\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-9 text-right\">\n\t\t\t`;\n\t\t\tlet query_params = frappe.utils.get_query_params();\n\t\t\tlet start = query_params.start ? cint(JSON.parse(query_params.start)) : 0;\n\t\t\tlet page_length = settings.products_per_page || 0;\n\n\t\t\tlet prev_disable = start > 0 ? \"\" : \"disabled\";\n\t\t\tlet next_disable = (this.product_count > page_length) ? \"\" : \"disabled\";\n\n\t\t\tpaging_html += `\n\t\t\t\t<button class=\"btn btn-default btn-prev\" data-start=\"${ start - page_length }\"\n\t\t\t\t\tstyle=\"float: left\" ${prev_disable}>\n\t\t\t\t\t${ __(\"Prev\") }\n\t\t\t\t</button>`;\n\n\t\t\tpaging_html += `\n\t\t\t\t<button class=\"btn btn-default btn-next\" data-start=\"${ start + page_length }\"\n\t\t\t\t\t${next_disable}>\n\t\t\t\t\t${ __(\"Next\") }\n\t\t\t\t</button>\n\t\t\t`;\n\n\t\t\tpaging_html += `</div></div>`;\n\n\t\t\t$(\".page_content\").append(paging_html);\n\t\t\tthis.bind_paging_action();\n\t\t}\n\t}\n\n\tprepare_search() {\n\t\t$(\".toolbar\").append(`\n\t\t\t<div class=\"input-group col-8 p-0\">\n\t\t\t\t<div class=\"dropdown w-100\" id=\"dropdownMenuSearch\">\n\t\t\t\t\t<input type=\"search\" name=\"query\" id=\"search-box\" class=\"form-control font-md\"\n\t\t\t\t\t\tplaceholder=\"${__(\"Search for Products\")}\"\n\t\t\t\t\t\taria-label=\"Product\" aria-describedby=\"button-addon2\">\n\t\t\t\t\t<div class=\"search-icon\">\n\t\t\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tstroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\n\t\t\t\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t\t\t\t\tclass=\"feather feather-search\">\n\t\t\t\t\t\t\t<circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n\t\t\t\t\t\t\t<line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- Results dropdown rendered in product_search.js -->\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t`);\n\t}\n\n\trender_view_toggler() {\n\t\t$(\".toolbar\").append(`<div class=\"toggle-container col-4 p-0\"></div>`);\n\n\t\t[\"btn-list-view\", \"btn-grid-view\"].forEach(view => {\n\t\t\tlet icon = view === \"btn-list-view\" ? \"list\" : \"image-view\";\n\t\t\t$(\".toggle-container\").append(`\n\t\t\t\t<div class=\"form-group mb-0\" id=\"toggle-view\">\n\t\t\t\t\t<button id=\"${ icon }\" class=\"btn ${ view } mr-2\">\n\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t<svg class=\"icon icon-md\">\n\t\t\t\t\t\t\t\t<use href=\"#icon-${ icon }\"></use>\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t`);\n\t\t});\n\t}\n\n\tbind_view_toggler_actions() {\n\t\t$(\"#list\").click(function() {\n\t\t\tlet $btn = $(this);\n\t\t\t$btn.removeClass('btn-primary');\n\t\t\t$btn.addClass('btn-primary');\n\t\t\t$(\".btn-grid-view\").removeClass('btn-primary');\n\n\t\t\t$(\"#products-grid-area\").addClass(\"hidden\");\n\t\t\t$(\"#products-list-area\").removeClass(\"hidden\");\n\t\t\tlocalStorage.setItem(\"product_view\", \"List View\");\n\t\t});\n\n\t\t$(\"#image-view\").click(function() {\n\t\t\tlet $btn = $(this);\n\t\t\t$btn.removeClass('btn-primary');\n\t\t\t$btn.addClass('btn-primary');\n\t\t\t$(\".btn-list-view\").removeClass('btn-primary');\n\n\t\t\t$(\"#products-list-area\").addClass(\"hidden\");\n\t\t\t$(\"#products-grid-area\").removeClass(\"hidden\");\n\t\t\tlocalStorage.setItem(\"product_view\", \"Grid View\");\n\t\t});\n\t}\n\n\tset_view_state() {\n\t\tif (this.preference === \"List View\") {\n\t\t\t$(\"#list\").addClass('btn-primary');\n\t\t\t$(\"#image-view\").removeClass('btn-primary');\n\t\t} else {\n\t\t\t$(\"#image-view\").addClass('btn-primary');\n\t\t\t$(\"#list\").removeClass('btn-primary');\n\t\t}\n\t}\n\n\tbind_paging_action() {\n\t\tlet me = this;\n\t\t$('.btn-prev, .btn-next').click((e) => {\n\t\t\tconst $btn = $(e.target);\n\t\t\tme.from_filters = false;\n\n\t\t\t$btn.prop('disabled', true);\n\t\t\tconst start = $btn.data('start');\n\n\t\t\tlet query_params = frappe.utils.get_query_params();\n\t\t\tquery_params.start = start;\n\t\t\tlet path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);\n\t\t\twindow.location.href = path;\n\t\t});\n\t}\n\n\tre_render_discount_filters(filter_data) {\n\t\tthis.get_discount_filter_html(filter_data);\n\t\tif (this.from_filters) {\n\t\t\t// Bind filter action if triggered via filters\n\t\t\t// if not from filter action, page load will bind actions\n\t\t\tthis.bind_discount_filter_action();\n\t\t}\n\t\t// discount filters are rendered with Items (later)\n\t\t// unlike the other filters\n\t\tthis.restore_discount_filter();\n\t}\n\n\tget_discount_filter_html(filter_data) {\n\t\t$(\"#discount-filters\").remove();\n\t\tif (filter_data) {\n\t\t\t$(\"#product-filters\").append(`\n\t\t\t\t<div id=\"discount-filters\" class=\"mb-4 filter-block pb-5\">\n\t\t\t\t\t<div class=\"filter-label mb-3\">${ __(\"Discounts\") }</div>\n\t\t\t\t</div>\n\t\t\t`);\n\n\t\t\tlet html = `<div class=\"filter-options\">`;\n\t\t\tfilter_data.forEach(filter => {\n\t\t\t\thtml += `\n\t\t\t\t\t<div class=\"checkbox\">\n\t\t\t\t\t\t<label data-value=\"${ filter[0] }\">\n\t\t\t\t\t\t\t<input type=\"radio\"\n\t\t\t\t\t\t\t\tclass=\"product-filter discount-filter\"\n\t\t\t\t\t\t\t\tname=\"discount\" id=\"${ filter[0] }\"\n\t\t\t\t\t\t\t\tdata-filter-name=\"discount\"\n\t\t\t\t\t\t\t\tdata-filter-value=\"${ filter[0] }\"\n\t\t\t\t\t\t\t\tstyle=\"width: 14px !important\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<span class=\"label-area\" for=\"${ filter[0] }\">\n\t\t\t\t\t\t\t\t\t${ filter[1] }\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</div>\n\t\t\t\t`;\n\t\t\t});\n\t\t\thtml += `</div>`;\n\n\t\t\t$(\"#discount-filters\").append(html);\n\t\t}\n\t}\n\n\trestore_discount_filter() {\n\t\tconst filters = frappe.utils.get_query_params();\n\t\tlet field_filters = filters.field_filters;\n\t\tif (!field_filters) return;\n\n\t\tfield_filters = JSON.parse(field_filters);\n\n\t\tif (field_filters && field_filters[\"discount\"]) {\n\t\t\tconst values = field_filters[\"discount\"];\n\t\t\tconst selector = values.map(value => {\n\t\t\t\treturn `input[data-filter-name=\"discount\"][data-filter-value=\"${value}\"]`;\n\t\t\t}).join(',');\n\t\t\t$(selector).prop('checked', true);\n\t\t\tthis.field_filters = field_filters;\n\t\t}\n\t}\n\n\tbind_discount_filter_action() {\n\t\tlet me = this;\n\t\t$('.discount-filter').on('change', (e) => {\n\t\t\tconst $checkbox = $(e.target);\n\t\t\tconst is_checked = $checkbox.is(':checked');\n\n\t\t\tconst {\n\t\t\t\tfilterValue: filter_value\n\t\t\t} = $checkbox.data();\n\n\t\t\tdelete this.field_filters[\"discount\"];\n\n\t\t\tif (is_checked) {\n\t\t\t\tthis.field_filters[\"discount\"] = [];\n\t\t\t\tthis.field_filters[\"discount\"].push(filter_value);\n\t\t\t}\n\n\t\t\tif (this.field_filters[\"discount\"].length === 0) {\n\t\t\t\tdelete this.field_filters[\"discount\"];\n\t\t\t}\n\n\t\t\tme.change_route_with_filters();\n\t\t});\n\t}\n\n\tbind_filters() {\n\t\tlet me = this;\n\t\tthis.field_filters = {};\n\t\tthis.attribute_filters = {};\n\n\t\t$('.product-filter').on('change', (e) => {\n\t\t\tme.from_filters = true;\n\n\t\t\tconst $checkbox = $(e.target);\n\t\t\tconst is_checked = $checkbox.is(':checked');\n\n\t\t\tif ($checkbox.is('.attribute-filter')) {\n\t\t\t\tconst {\n\t\t\t\t\tattributeName: attribute_name,\n\t\t\t\t\tattributeValue: attribute_value\n\t\t\t\t} = $checkbox.data();\n\n\t\t\t\tif (is_checked) {\n\t\t\t\t\tthis.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];\n\t\t\t\t\tthis.attribute_filters[attribute_name].push(attribute_value);\n\t\t\t\t} else {\n\t\t\t\t\tthis.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];\n\t\t\t\t\tthis.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);\n\t\t\t\t}\n\n\t\t\t\tif (this.attribute_filters[attribute_name].length === 0) {\n\t\t\t\t\tdelete this.attribute_filters[attribute_name];\n\t\t\t\t}\n\t\t\t} else if ($checkbox.is('.field-filter') || $checkbox.is('.discount-filter')) {\n\t\t\t\tconst {\n\t\t\t\t\tfilterName: filter_name,\n\t\t\t\t\tfilterValue: filter_value\n\t\t\t\t} = $checkbox.data();\n\n\t\t\t\tif ($checkbox.is('.discount-filter')) {\n\t\t\t\t\t// clear previous discount filter to accomodate new\n\t\t\t\t\tdelete this.field_filters[\"discount\"];\n\t\t\t\t}\n\t\t\t\tif (is_checked) {\n\t\t\t\t\tthis.field_filters[filter_name] = this.field_filters[filter_name] || [];\n\t\t\t\t\tif (!in_list(this.field_filters[filter_name], filter_value)) {\n\t\t\t\t\t\tthis.field_filters[filter_name].push(filter_value);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.field_filters[filter_name] = this.field_filters[filter_name] || [];\n\t\t\t\t\tthis.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);\n\t\t\t\t}\n\n\t\t\t\tif (this.field_filters[filter_name].length === 0) {\n\t\t\t\t\tdelete this.field_filters[filter_name];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tme.change_route_with_filters();\n\t\t});\n\n\t\t// bind filter lookup input box\n\t\t$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {\n\t\t\tconst $input = $(e.target);\n\t\t\tconst keyword = ($input.val() || '').toLowerCase();\n\t\t\tconst $filter_options = $input.next('.filter-options');\n\n\t\t\t$filter_options.find('.filter-lookup-wrapper').show();\n\t\t\t$filter_options.find('.filter-lookup-wrapper').each((i, el) => {\n\t\t\t\tconst $el = $(el);\n\t\t\t\tconst value = $el.data('value').toLowerCase();\n\t\t\t\tif (!value.includes(keyword)) {\n\t\t\t\t\t$el.hide();\n\t\t\t\t}\n\t\t\t});\n\t\t}, 300));\n\t}\n\n\tchange_route_with_filters() {\n\t\tlet route_params = frappe.utils.get_query_params();\n\n\t\tlet start = this.if_key_exists(route_params.start) || 0;\n\t\tif (this.from_filters) {\n\t\t\tstart = 0; // show items from first page if new filters are triggered\n\t\t}\n\n\t\tconst query_string = this.get_query_string({\n\t\t\tstart: start,\n\t\t\tfield_filters: JSON.stringify(this.if_key_exists(this.field_filters)),\n\t\t\tattribute_filters: JSON.stringify(this.if_key_exists(this.attribute_filters)),\n\t\t});\n\t\twindow.history.pushState('filters', '', `${location.pathname}?` + query_string);\n\n\t\t$('.page_content input').prop('disabled', true);\n\n\t\tthis.make(true);\n\t\t$('.page_content input').prop('disabled', false);\n\t}\n\n\trestore_filters_state() {\n\t\tconst filters = frappe.utils.get_query_params();\n\t\tlet {field_filters, attribute_filters} = filters;\n\n\t\tif (field_filters) {\n\t\t\tfield_filters = JSON.parse(field_filters);\n\t\t\tfor (let fieldname in field_filters) {\n\t\t\t\tconst values = field_filters[fieldname];\n\t\t\t\tconst selector = values.map(value => {\n\t\t\t\t\treturn `input[data-filter-name=\"${fieldname}\"][data-filter-value=\"${value}\"]`;\n\t\t\t\t}).join(',');\n\t\t\t\t$(selector).prop('checked', true);\n\t\t\t}\n\t\t\tthis.field_filters = field_filters;\n\t\t}\n\t\tif (attribute_filters) {\n\t\t\tattribute_filters = JSON.parse(attribute_filters);\n\t\t\tfor (let attribute in attribute_filters) {\n\t\t\t\tconst values = attribute_filters[attribute];\n\t\t\t\tconst selector = values.map(value => {\n\t\t\t\t\treturn `input[data-attribute-name=\"${attribute}\"][data-attribute-value=\"${value}\"]`;\n\t\t\t\t}).join(',');\n\t\t\t\t$(selector).prop('checked', true);\n\t\t\t}\n\t\t\tthis.attribute_filters = attribute_filters;\n\t\t}\n\t}\n\n\trender_no_products_section(error=false) {\n\t\tlet error_section = `\n\t\t\t<div class=\"mt-4 w-100 alert alert-error font-md\">\n\t\t\t\t${ __(\"Something went wrong. Please refresh or contact us.\") }\n\t\t\t</div>\n\t\t`;\n\t\tlet no_results_section = `\n\t\t\t<div class=\"cart-empty frappe-card mt-4\">\n\t\t\t\t<div class=\"cart-empty-state\">\n\t\t\t\t\t<img src=\"/assets/webshop/images/cart-empty-state.png\" alt=\"Empty Cart\">\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cart-empty-message mt-4\">${ __(\"No products found\") }</p>\n\t\t\t</div>\n\t\t`;\n\n\t\tthis.products_section.append(error ? error_section : no_results_section);\n\t}\n\n\trender_item_sub_categories(categories) {\n\t\tif (categories && categories.length) {\n\t\t\tlet sub_group_html = `\n\t\t\t\t<div class=\"sub-category-container scroll-categories\">\n\t\t\t`;\n\n\t\t\tcategories.forEach(category => {\n\t\t\t\tsub_group_html += `\n\t\t\t\t\t<a href=\"/${ category.route || '#' }\" style=\"text-decoration: none;\">\n\t\t\t\t\t\t<div class=\"category-pill\">\n\t\t\t\t\t\t\t${ category.name }\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</a>\n\t\t\t\t`;\n\t\t\t});\n\t\t\tsub_group_html += `</div>`;\n\n\t\t\t$(\"#product-listing\").prepend(sub_group_html);\n\t\t}\n\t}\n\n\tget_query_string(object) {\n\t\tconst url = new URLSearchParams();\n\t\tfor (let key in object) {\n\t\t\tconst value = object[key];\n\t\t\tif (value) {\n\t\t\t\turl.append(key, value);\n\t\t\t}\n\t\t}\n\t\treturn url.toString();\n\t}\n\n\tif_key_exists(obj) {\n\t\tlet exists = false;\n\t\tfor (let key in obj) {\n\t\t\tif (Object.prototype.hasOwnProperty.call(obj, key) && obj[key]) {\n\t\t\t\texists = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn exists ? obj : undefined;\n\t}\n};\n"
  },
  {
    "path": "webshop/public/js/shopping_cart.js",
    "content": "// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n// License: GNU General Public License v3. See license.txt\n\n// shopping cart\nfrappe.provide(\"webshop.webshop.shopping_cart\");\nvar shopping_cart = webshop.webshop.shopping_cart;\n\nvar getParams = function (url) {\n\tvar params = [];\n\tvar parser = document.createElement('a');\n\tparser.href = url;\n\tvar query = parser.search.substring(1);\n\tvar vars = query.split('&');\n\tfor (var i = 0; i < vars.length; i++) {\n\t\tvar pair = vars[i].split('=');\n\t\tparams[pair[0]] = decodeURIComponent(pair[1]);\n\t}\n\treturn params;\n};\n\nfrappe.ready(function() {\n\tvar full_name = frappe.session && frappe.session.user_fullname;\n\t// update user\n\tif(full_name) {\n\t\t$('.navbar li[data-label=\"User\"] a')\n\t\t\t.html('<i class=\"fa fa-fixed-width fa fa-user\"></i> ' + full_name);\n\t}\n\t// set coupon code and sales partner code\n\n\tvar url_args = getParams(window.location.href);\n\n\tvar referral_coupon_code = url_args['cc'];\n\tvar referral_sales_partner = url_args['sp'];\n\n\tvar d = new Date();\n\t// expires within 30 minutes\n\td.setTime(d.getTime() + (0.02 * 24 * 60 * 60 * 1000));\n\tvar expires = \"expires=\"+d.toUTCString();\n\tif (referral_coupon_code) {\n\t\tdocument.cookie = \"referral_coupon_code=\" + referral_coupon_code + \";\" + expires + \";path=/\";\n\t}\n\tif (referral_sales_partner) {\n\t\tdocument.cookie = \"referral_sales_partner=\" + referral_sales_partner + \";\" + expires + \";path=/\";\n\t}\n\treferral_coupon_code=frappe.get_cookie(\"referral_coupon_code\");\n\treferral_sales_partner=frappe.get_cookie(\"referral_sales_partner\");\n\n\tif (referral_coupon_code && $(\".tot_quotation_discount\").val()==undefined ) {\n\t\t$(\".txtcoupon\").val(referral_coupon_code);\n\t}\n\tif (referral_sales_partner) {\n\t\t$(\".txtreferral_sales_partner\").val(referral_sales_partner);\n\t}\n\n\t// update login\n\tshopping_cart.show_shoppingcart_dropdown();\n\tshopping_cart.set_cart_count();\n\tshopping_cart.show_cart_navbar();\n});\n\n$.extend(shopping_cart, {\n\tshow_shoppingcart_dropdown: function() {\n\t\t$(\".shopping-cart\").on('shown.bs.dropdown', function() {\n\t\t\tif (!$('.shopping-cart-menu .cart-container').length) {\n\t\t\t\treturn frappe.call({\n\t\t\t\t\tmethod: 'webshop.webshop.shopping_cart.cart.get_shopping_cart_menu',\n\t\t\t\t\tcallback: function(r) {\n\t\t\t\t\t\tif (r.message) {\n\t\t\t\t\t\t\t$('.shopping-cart-menu').html(r.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t},\n\n\tupdate_cart: function(opts) {\n\t\tif (frappe.session.user===\"Guest\") {\n\t\t\tif (localStorage) {\n\t\t\t\tlocalStorage.setItem(\"last_visited\", window.location.pathname);\n\t\t\t}\n\t\t\tfrappe.call('webshop.webshop.api.get_guest_redirect_on_action').then((res) => {\n\t\t\t\twindow.location.href = res.message || \"/login\";\n\t\t\t});\n\t\t} else {\n\t\t\tshopping_cart.freeze();\n\t\t\treturn frappe.call({\n\t\t\t\ttype: \"POST\",\n\t\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.update_cart\",\n\t\t\t\targs: {\n\t\t\t\t\titem_code: opts.item_code,\n\t\t\t\t\tqty: opts.qty,\n\t\t\t\t\tadditional_notes: opts.additional_notes !== undefined ? opts.additional_notes : undefined,\n\t\t\t\t\twith_items: opts.with_items || 0\n\t\t\t\t},\n\t\t\t\tbtn: opts.btn,\n\t\t\t\tcallback: function(r) {\n\t\t\t\t\tshopping_cart.unfreeze();\n\t\t\t\t\tshopping_cart.set_cart_count(true);\n\t\t\t\t\tif(opts.callback)\n\t\t\t\t\t\topts.callback(r);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t},\n\n\tset_cart_count: function(animate=false) {\n\t\t$(\".intermediate-empty-cart\").remove();\n\n\t\tvar cart_count = frappe.get_cookie(\"cart_count\");\n\t\tif(frappe.session.user===\"Guest\") {\n\t\t\tcart_count = 0;\n\t\t}\n\n\t\tif(cart_count) {\n\t\t\t$(\".shopping-cart\").toggleClass('hidden', false);\n\t\t}\n\n\t\tvar $cart = $('.cart-icon');\n\t\tvar $badge = $cart.find(\"#cart-count\");\n\n\t\tif(parseInt(cart_count) === 0 || cart_count === undefined) {\n\t\t\t$cart.css(\"display\", \"none\");\n\t\t\t$(\".cart-tax-items\").hide();\n\t\t\t$(\".btn-place-order\").hide();\n\t\t\t$(\".cart-payment-addresses\").hide();\n\n\t\t\tlet intermediate_empty_cart_msg = `\n\t\t\t\t<div class=\"text-center w-100 intermediate-empty-cart mt-4 mb-4 text-muted\">\n\t\t\t\t\t${ __(\"Cart is Empty\") }\n\t\t\t\t</div>\n\t\t\t`;\n\t\t\t$(\".cart-table\").after(intermediate_empty_cart_msg);\n\t\t}\n\t\telse {\n\t\t\t$cart.css(\"display\", \"inline\");\n\t\t\t$(\"#cart-count\").text(cart_count);\n\t\t}\n\n\t\tif(cart_count) {\n\t\t\t$badge.html(cart_count);\n\n\t\t\tif (animate) {\n\t\t\t\t$cart.addClass(\"cart-animate\");\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t$cart.removeClass(\"cart-animate\");\n\t\t\t\t}, 500);\n\t\t\t}\n\t\t} else {\n\t\t\t$badge.remove();\n\t\t}\n\t},\n\n\tshopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) {\n\t\tshopping_cart.update_cart({\n\t\t\titem_code,\n\t\t\tqty,\n\t\t\tadditional_notes,\n\t\t\twith_items: 1,\n\t\t\tbtn: this,\n\t\t\tcallback: function(r) {\n\t\t\t\tif(!r.exc) {\n\t\t\t\t\t$(\".cart-items\").html(r.message.items);\n\t\t\t\t\t$(\".cart-tax-items\").html(r.message.total);\n\t\t\t\t\t$(\".payment-summary\").html(r.message.taxes_and_totals);\n\t\t\t\t\tshopping_cart.set_cart_count();\n\n\t\t\t\t\tif (cart_dropdown != true) {\n\t\t\t\t\t\t$(\".cart-icon\").hide();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t},\n\n\tshow_cart_navbar: function () {\n\t\tfrappe.call({\n\t\t\tmethod: \"webshop.webshop.doctype.webshop_settings.webshop_settings.is_cart_enabled\",\n\t\t\tcallback: function(r) {\n\t\t\t\t$(\".shopping-cart\").toggleClass('hidden', r.message ? false : true);\n\t\t\t}\n\t\t});\n\t},\n\n\ttoggle_button_class(button, remove, add) {\n\t\tbutton.removeClass(remove);\n\t\tbutton.addClass(add);\n\t},\n\n\tbind_add_to_cart_action() {\n\t\t$('.page_content').on('click', '.btn-add-to-cart-list', (e) => {\n\t\t\tconst $btn = $(e.currentTarget);\n\t\t\t$btn.prop('disabled', true);\n\n\t\t\tif (frappe.session.user===\"Guest\") {\n\t\t\t\tif (localStorage) {\n\t\t\t\t\tlocalStorage.setItem(\"last_visited\", window.location.pathname);\n\t\t\t\t}\n\t\t\t\tfrappe.call('webshop.webshop.api.get_guest_redirect_on_action').then((res) => {\n\t\t\t\t\twindow.location.href = res.message || \"/login\";\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$btn.addClass('hidden');\n\t\t\t$btn.closest('.cart-action-container').addClass('d-flex');\n\t\t\t$btn.parent().find('.go-to-cart').removeClass('hidden');\n\t\t\t$btn.parent().find('.go-to-cart-grid').removeClass('hidden');\n\t\t\t$btn.parent().find('.cart-indicator').removeClass('hidden');\n\n\t\t\tconst item_code = $btn.data('item-code');\n\t\t\twebshop.webshop.shopping_cart.update_cart({\n\t\t\t\titem_code,\n\t\t\t\tqty: 1\n\t\t\t});\n\n\t\t});\n\t},\n\n\tfreeze() {\n\t\tif (window.location.pathname !== \"/cart\") return;\n\n\t\tif (!$('#freeze').length) {\n\t\t\tlet freeze = $('<div id=\"freeze\" class=\"modal-backdrop fade\"></div>')\n\t\t\t\t.appendTo(\"body\");\n\n\t\t\tsetTimeout(function() {\n\t\t\t\tfreeze.addClass(\"show\");\n\t\t\t}, 1);\n\t\t} else {\n\t\t\t$(\"#freeze\").addClass(\"show\");\n\t\t}\n\t},\n\n\tunfreeze() {\n\t\tif ($('#freeze').length) {\n\t\t\tlet freeze = $('#freeze').removeClass(\"show\");\n\t\t\tsetTimeout(function() {\n\t\t\t\tfreeze.remove();\n\t\t\t}, 1);\n\t\t}\n\t}\n});\n"
  },
  {
    "path": "webshop/public/js/wishlist.js",
    "content": "frappe.provide(\"webshop.webshop.wishlist\");\nvar wishlist = webshop.webshop.wishlist;\n\nfrappe.provide(\"webshop.webshop.shopping_cart\");\nvar shopping_cart = webshop.webshop.shopping_cart;\n\n$.extend(wishlist, {\n\tset_wishlist_count: function(animate=false) {\n\t\t// set badge count for wishlist icon\n\t\tvar wish_count = frappe.get_cookie(\"wish_count\");\n\t\tif (frappe.session.user===\"Guest\") {\n\t\t\twish_count = 0;\n\t\t}\n\n\t\tif (wish_count) {\n\t\t\t$(\".wishlist\").toggleClass('hidden', false);\n\t\t}\n\n\t\tvar $wishlist = $('.wishlist-icon');\n\t\tvar $badge = $wishlist.find(\"#wish-count\");\n\n\t\tif (parseInt(wish_count) === 0 || wish_count === undefined) {\n\t\t\t$wishlist.css(\"display\", \"none\");\n\t\t} else {\n\t\t\t$wishlist.css(\"display\", \"inline\");\n\t\t}\n\t\tif (wish_count) {\n\t\t\t$badge.html(wish_count);\n\t\t\tif (animate) {\n\t\t\t\t$wishlist.addClass('cart-animate');\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t$wishlist.removeClass('cart-animate');\n\t\t\t\t}, 500);\n\t\t\t}\n\t\t} else {\n\t\t\t$badge.remove();\n\t\t}\n\t},\n\n\tbind_move_to_cart_action: function() {\n\t\t// move item to cart from wishlist\n\t\t$('.page_content').on(\"click\", \".btn-add-to-cart\", (e) => {\n\t\t\tconst $move_to_cart_btn = $(e.currentTarget);\n\t\t\tlet item_code = $move_to_cart_btn.data(\"item-code\");\n\n\t\t\tshopping_cart.shopping_cart_update({\n\t\t\t\titem_code,\n\t\t\t\tqty: 1,\n\t\t\t\tcart_dropdown: true\n\t\t\t});\n\n\t\t\tlet success_action = function() {\n\t\t\t\tconst $card_wrapper = $move_to_cart_btn.closest(\".wishlist-card\");\n\t\t\t\t$card_wrapper.addClass(\"wish-removed\");\n\t\t\t};\n\t\t\tlet args = { item_code: item_code };\n\t\t\tthis.add_remove_from_wishlist(\"remove\", args, success_action, null, true);\n\t\t});\n\t},\n\n\tbind_remove_action: function() {\n\t\t// remove item from wishlist\n\t\tlet me = this;\n\n\t\t$('.page_content').on(\"click\", \".remove-wish\", (e) => {\n\t\t\tconst $remove_wish_btn = $(e.currentTarget);\n\t\t\tlet item_code = $remove_wish_btn.data(\"item-code\");\n\n\t\t\tlet success_action = function() {\n\t\t\t\tconst $card_wrapper = $remove_wish_btn.closest(\".wishlist-card\");\n\t\t\t\t$card_wrapper.addClass(\"wish-removed\");\n\t\t\t\tif (frappe.get_cookie(\"wish_count\") == 0) {\n\t\t\t\t\t$(\".page_content\").empty();\n\t\t\t\t\tme.render_empty_state();\n\t\t\t\t}\n\t\t\t};\n\t\t\tlet args = { item_code: item_code };\n\t\t\tthis.add_remove_from_wishlist(\"remove\", args, success_action);\n\t\t});\n\t},\n\n\tbind_wishlist_action() {\n\t\t// 'wish'('like') or 'unwish' item in product listing\n\t\t$('.page_content').on('click', '.like-action, .like-action-list', (e) => {\n\t\t\tconst $btn = $(e.currentTarget);\n\t\t\tthis.wishlist_action($btn);\n\t\t});\n\t},\n\n\twishlist_action(btn) {\n\t\tconst $wish_icon = btn.find('.wish-icon');\n\t\tlet me = this;\n\n\t\tif (frappe.session.user===\"Guest\") {\n\t\t\tif (localStorage) {\n\t\t\t\tlocalStorage.setItem(\"last_visited\", window.location.pathname);\n\t\t\t}\n\t\t\tthis.redirect_guest();\n\t\t\treturn;\n\t\t}\n\n\t\tlet success_action = function() {\n\t\t\twebshop.webshop.wishlist.set_wishlist_count(true);\n\t\t};\n\n\t\tif ($wish_icon.hasClass('wished')) {\n\t\t\t// un-wish item\n\t\t\tbtn.removeClass(\"like-animate\");\n\t\t\tbtn.addClass(\"like-action-wished\");\n\t\t\tthis.toggle_button_class($wish_icon, 'wished', 'not-wished');\n\n\t\t\tlet args = { item_code: btn.data('item-code') };\n\t\t\tlet failure_action = function() {\n\t\t\t\tme.toggle_button_class($wish_icon, 'not-wished', 'wished');\n\t\t\t};\n\t\t\tthis.add_remove_from_wishlist(\"remove\", args, success_action, failure_action);\n\t\t} else {\n\t\t\t// wish item\n\t\t\tbtn.addClass(\"like-animate\");\n\t\t\tbtn.addClass(\"like-action-wished\");\n\t\t\tthis.toggle_button_class($wish_icon, 'not-wished', 'wished');\n\n\t\t\tlet args = {item_code: btn.data('item-code')};\n\t\t\tlet failure_action = function() {\n\t\t\t\tme.toggle_button_class($wish_icon, 'wished', 'not-wished');\n\t\t\t};\n\t\t\tthis.add_remove_from_wishlist(\"add\", args, success_action, failure_action);\n\t\t}\n\t},\n\n\ttoggle_button_class(button, remove, add) {\n\t\tbutton.removeClass(remove);\n\t\tbutton.addClass(add);\n\t},\n\n\tadd_remove_from_wishlist(action, args, success_action, failure_action, async=false) {\n\t\t/*\tAJAX call to add or remove Item from Wishlist\n\t\t\taction: \"add\" or \"remove\"\n\t\t\targs: args for method (item_code, price, formatted_price),\n\t\t\tsuccess_action: method to execute on successs,\n\t\t\tfailure_action: method to execute on failure,\n\t\t\tasync: make call asynchronously (true/false).\t*/\n\t\tif (frappe.session.user===\"Guest\") {\n\t\t\tif (localStorage) {\n\t\t\t\tlocalStorage.setItem(\"last_visited\", window.location.pathname);\n\t\t\t}\n\t\t\tthis.redirect_guest();\n\t\t} else {\n\t\t\tlet method = \"webshop.webshop.doctype.wishlist.wishlist.add_to_wishlist\";\n\t\t\tif (action === \"remove\") {\n\t\t\t\tmethod = \"webshop.webshop.doctype.wishlist.wishlist.remove_from_wishlist\";\n\t\t\t}\n\n\t\t\tfrappe.call({\n\t\t\t\tasync: async,\n\t\t\t\ttype: \"POST\",\n\t\t\t\tmethod: method,\n\t\t\t\targs: args,\n\t\t\t\tcallback: function (r) {\n\t\t\t\t\tif (r.exc) {\n\t\t\t\t\t\tif (failure_action && (typeof failure_action === 'function')) {\n\t\t\t\t\t\t\tfailure_action();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfrappe.msgprint({\n\t\t\t\t\t\t\tmessage: __(\"Sorry, something went wrong. Please refresh.\"),\n\t\t\t\t\t\t\tindicator: \"red\", title: __(\"Note\")\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (success_action && (typeof success_action === 'function')) {\n\t\t\t\t\t\tsuccess_action();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t},\n\n\tredirect_guest() {\n\t\tfrappe.call('webshop.webshop.api.get_guest_redirect_on_action').then((res) => {\n\t\t\twindow.location.href = res.message || \"/login\";\n\t\t});\n\t},\n\n\trender_empty_state() {\n\t\t$(\".page_content\").append(`\n\t\t\t<div class=\"cart-empty frappe-card\">\n\t\t\t\t<div class=\"cart-empty-state\">\n\t\t\t\t\t<img src=\"/assets/webshop/images/cart-empty-state.png\" alt=\"Empty Cart\">\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cart-empty-message mt-4\">${ __('Wishlist is empty !') }</p>\n\t\t\t</div>\n\t\t`);\n\t}\n\n});\n\nfrappe.ready(function() {\n\tif (window.location.pathname !== \"/wishlist\") {\n\t\t$(\".wishlist\").toggleClass('hidden', true);\n\t\twishlist.set_wishlist_count();\n\t} else {\n\t\twishlist.bind_move_to_cart_action();\n\t\twishlist.bind_remove_action();\n\t}\n\n});\n"
  },
  {
    "path": "webshop/public/scss/webshop-web.bundle.scss",
    "content": "@import \"./webshop_cart\";"
  },
  {
    "path": "webshop/public/scss/webshop_cart.scss",
    "content": "@import \"frappe/public/scss/common/mixins\";\n\n:root {\n\t--green-info: #38A160;\n\t--product-bg-color: white;\n\t--body-bg-color:  var(--gray-50);\n}\n\nbody.product-page {\n\tbackground: var(--body-bg-color);\n}\n\n.item-breadcrumbs {\n\t.breadcrumb-container {\n\t\ta {\n\t\t\tcolor: var(--gray-900);\n\t\t}\n\t}\n}\n\n.carousel-control {\n\theight: 42px;\n\twidth: 42px;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground: white;\n\tbox-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08), 0px 1px 2px 1px rgba(0, 0, 0, 0.06);\n\tborder-radius: 100px;\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n\topacity: 1;\n\twidth: 8%;\n\n\t@media (max-width: 1200px) {\n\t\twidth: 10%;\n\t}\n\t@media (max-width: 768px) {\n\t\twidth: 15%;\n\t}\n}\n\n.carousel-body {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n}\n\n.carousel-content {\n\tmax-width: 400px;\n\tmargin-left: 5rem;\n\tmargin-right: 5rem;\n}\n\n.card {\n\tborder: none;\n}\n\n.product-category-section {\n\t.card:hover {\n\t\tbox-shadow: 0px 16px 45px 6px rgba(0, 0, 0, 0.08), 0px 8px 10px -10px rgba(0, 0, 0, 0.04);\n\t}\n\n\t.card-grid {\n\t\tdisplay: grid;\n\t\tgrid-gap: 15px;\n\t\tgrid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\n\t}\n}\n\n.no-image-item {\n\theight: 340px;\n\twidth: 340px;\n\tbackground: var(--gray-100);\n\tborder-radius: var(--border-radius);\n\tfont-size: 2rem;\n\tcolor: var(--gray-500);\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.item-card-group-section {\n\t.card {\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\n\t\t&:hover {\n\t\t\tbox-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);\n\t\t\ttransition: box-shadow 400ms;\n\t\t}\n\t}\n\n\t.card:hover, .card:focus-within {\n\t\t.btn-add-to-cart-list {\n\t\t\tvisibility: visible;\n\t\t}\n\t\t.like-action {\n\t\t\tvisibility: visible;\n\t\t}\n\t\t.btn-explore-variants {\n\t\t\tvisibility: visible;\n\t\t}\n\t}\n\n\n\t.card-img-container {\n\t\theight: 210px;\n\t\twidth: 100%;\n\t}\n\n\t.card-img {\n\t\tmax-height: 210px;\n\t\tobject-fit: contain;\n\t\tmargin-top: 1.25rem;\n\t}\n\n\t.no-image {\n\t\t@include flex(flex, center, center, null);\n\t\theight: 220px;\n\t\tbackground: var(--gray-100);\n\t\twidth: 100%;\n\t\tborder-radius: var(--border-radius) var(--border-radius) 0 0;\n\t\tfont-size: 2rem;\n\t\tcolor: var(--gray-500);\n\t}\n\n\t.no-image-list {\n\t\t@include flex(flex, center, center, null);\n\t\theight: 150px;\n\t\tbackground: var(--gray-100);\n\t\tborder-radius: var(--border-radius);\n\t\tfont-size: 2rem;\n\t\tcolor: var(--gray-500);\n\t\tmargin-top: 15px;\n\t\tmargin-bottom: 15px;\n\t}\n\n\t.card-body-flex {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t}\n\n\t.product-title {\n\t\tfont-size: 14px;\n\t\tcolor: var(--gray-800);\n\t\tfont-weight: 500;\n\t}\n\n\t.product-description {\n\t\tfont-size: 12px;\n\t\tcolor: var(--text-color);\n\t\tmargin: 20px 0;\n\t\tdisplay: -webkit-box;\n\t\t-webkit-line-clamp: 6;\n\t\t-webkit-box-orient: vertical;\n\n\t\tp {\n\t\t\tmargin-bottom: 0.5rem;\n\t\t}\n\t}\n\n\t.product-category {\n\t\tfont-size: 13px;\n\t\tcolor: var(--text-muted);\n\t\tmargin: var(--margin-sm) 0;\n\t}\n\n\t.product-price {\n\t\tfont-size: 18px;\n\t\tfont-weight: 600;\n\t\tcolor: var(--text-color);\n\t\tmargin: var(--margin-sm) 0;\n\t\tmargin-bottom: auto !important;\n\n\t\t.striked-price {\n\t\t\tfont-weight: 500;\n\t\t\tfont-size: 15px;\n\t\t\tcolor: var(--gray-500);\n\t\t}\n\t}\n\n\t.product-info-green {\n\t\tcolor: var(--green-info);\n\t\tfont-weight: 600;\n\t}\n\n\t.item-card {\n\t\tpadding: var(--padding-sm);\n\t\tmin-width: 300px;\n\t}\n\n\t.wishlist-card {\n\t\tpadding: var(--padding-sm);\n\t\tmin-width: 260px;\n\t\t.card-body-flex {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t}\n\t}\n}\n\n#products-list-area, #products-grid-area {\n\tpadding: 0 5px;\n}\n\n.list-row {\n\tbackground-color: white;\n\tpadding-bottom: 1rem;\n\tpadding-top: 1.5rem !important;\n\tborder-radius: 8px;\n\tborder-bottom: 1px solid var(--gray-50);\n\n\t&:hover, &:focus-within {\n\t\tbox-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);\n\t\ttransition: box-shadow 400ms;\n\n\t\t.btn-add-to-cart-list {\n\t\t\tvisibility: visible;\n\t\t}\n\t\t.like-action-list {\n\t\t\tvisibility: visible;\n\t\t}\n\t\t.btn-explore-variants {\n\t\t\tvisibility: visible;\n\t\t}\n\t}\n\n\t.product-code {\n\t\tpadding-top: 0 !important;\n\t}\n\n\t.btn-explore-variants {\n\t\tmin-width: 135px;\n\t\tmax-height: 30px;\n\t\tfloat: right;\n\t\tpadding: 0.25rem 1rem;\n\t}\n}\n\n[data-doctype=\"Item Group\"],\n#page-index {\n\t.page-header {\n\t\tfont-size: 20px;\n\t\tfont-weight: 700;\n\t\tcolor: var(--text-color);\n\t}\n\n\t.filters-section {\n\t\t.title-section {\n\t\t\tborder-bottom: 1px solid var(--table-border-color);\n\t\t}\n\n\t\t.filter-title {\n\t\t\tfont-weight: 500;\n\t\t}\n\n\t\t.clear-filters {\n\t\t\tfont-size: 13px;\n\t\t}\n\n\t\t.filter-lookup-input {\n\t\t\tbackground-color: white;\n\t\t\tborder: 1px solid var(--gray-300);\n\n\t\t\t&:focus {\n\t\t\t\tborder: 1px solid var(--primary);\n\t\t\t}\n\t\t}\n\n\t\t.filter-label {\n\t\t\tfont-size: 11px;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: var(--gray-700);\n\t\t\ttext-transform: uppercase;\n\t\t}\n\n\t\t.filter-block {\n\t\t\tborder-bottom: 1px solid var(--table-border-color);\n\t\t}\n\n\t\t.checkbox {\n\t\t\t.label-area {\n\t\t\t\tfont-size: 13px;\n\t\t\t\tcolor: var(--gray-800);\n\t\t\t}\n\t\t}\n\t}\n}\n\n.product-filter {\n\twidth: 14px !important;\n\theight: 14px !important;\n}\n\n.discount-filter {\n\t&:before {\n\t\twidth: 14px !important;\n\t\theight: 14px !important;\n\t}\n}\n\n.list-image {\n\tborder: none !important;\n\toverflow: hidden;\n\tmax-height: 200px;\n\tbackground-color: white;\n}\n\n.product-container {\n\t@include card($padding: var(--padding-md));\n\tbackground-color: var(--product-bg-color) !important;\n\tmin-height: fit-content;\n\n\t.product-details {\n\t\tmax-width: 50%;\n\n\t\t.btn-add-to-cart {\n\t\t\tfont-size: 14px;\n\t\t}\n\t}\n\n\t&.item-main {\n\t\t.product-image {\n\t\t\twidth: 100%;\n\t\t}\n\t}\n\n\t.expand {\n\t\tmax-width: 100% !important; // expand in absence of slideshow\n\t}\n\n\t@media (max-width: 789px) {\n\t\t.product-details {\n\t\t\tmax-width: 90% !important;\n\n\t\t\t.btn-add-to-cart {\n\t\t\t\tfont-size: 14px;\n\t\t\t}\n\t\t}\n\t}\n\n\t.btn-add-to-wishlist {\n\t\tsvg use {\n\t\t\t--icon-stroke: #F47A7A;\n\t\t}\n\t}\n\n\t.btn-view-in-wishlist {\n\t\tsvg use {\n\t\t\tfill: #F47A7A;\n\t\t\t--icon-stroke: none;\n\t\t}\n\t}\n\n\t.product-title {\n\t\tfont-size: 16px;\n\t\tfont-weight: 600;\n\t\tcolor: var(--text-color);\n\t\tpadding: 0 !important;\n\t}\n\n\t.product-description {\n\t\tfont-size: 13px;\n\t\tcolor: var(--gray-800);\n\t}\n\n\t.product-image {\n\t\tborder-color: var(--table-border-color) !important;\n\t\tpadding: 15px;\n\n\t\t@media (max-width: var(--md-width)) {\n\t\t\theight: 300px;\n\t\t\twidth: 300px;\n\t\t}\n\n\t\t@media (min-width: var(--lg-width)) {\n\t\t\theight: 350px;\n\t\t\twidth: 350px;\n\t\t}\n\n\t \timg {\n\t\t\tobject-fit: contain;\n\t\t}\n\t}\n\n\t.item-slideshow {\n\n\t\t@media (max-width: var(--md-width)) {\n\t\t\tmax-height: 320px;\n\t\t}\n\n\t\t@media (min-width: var(--lg-width)) {\n\t\t\tmax-height: 430px;\n\t\t}\n\n\t\toverflow: auto;\n\t}\n\n\t.item-slideshow-image {\n\t\theight: 4rem;\n\t\twidth: 6rem;\n\t\tobject-fit: contain;\n\t\tpadding: 0.5rem;\n\t\tborder: 1px solid var(--table-border-color);\n\t\tborder-radius: 4px;\n\t\tcursor: pointer;\n\n\t\t&:hover, &.active {\n\t\t\tborder-color: var(--primary);\n\t\t}\n\t}\n\n\t.item-cart {\n\t\t.product-price {\n\t\t\tfont-size: 22px;\n\t\t\tcolor: var(--text-color);\n\t\t\tfont-weight: 600;\n\n\t\t\t.formatted-price {\n\t\t\t\tcolor: var(--text-muted);\n\t\t\t\tfont-size: 14px;\n\t\t\t}\n\t\t}\n\n\t\t.no-stock {\n\t\t\tfont-size: var(--text-base);\n\t\t}\n\n\t\t.offers-heading {\n\t\t\tfont-size: 16px !important;\n\t\t\tcolor: var(--text-color);\n\t\t\t.tag-icon {\n\t\t\t\t--icon-stroke: var(--gray-500);\n\t\t\t}\n\t\t}\n\n\t\t.w-30-40 {\n\t\t\twidth: 30%;\n\n\t\t\t@media (max-width: 992px) {\n\t\t\t\twidth: 40%;\n\t\t\t}\n\t\t}\n\t}\n\n\t.tab-content {\n\t\tfont-size: 14px;\n\t}\n}\n\n// Item Recommendations\n.recommended-item-section {\n\tpadding-right: 0;\n\n\t.recommendation-header {\n\t\tfont-size: 16px;\n\t\tfont-weight: 500\n\t}\n\n\t.recommendation-container {\n\t\tpadding: .5rem;\n\t\tmin-height: 0px;\n\n\t\t.r-item-image {\n\t\t\tmin-height: 100px;\n\t\t\twidth: 40%;\n\n\t\t\t.r-product-image {\n\t\t\t\tpadding: 2px 15px;\n\t\t\t}\n\n\t\t\t.no-image-r-item {\n\t\t\t\tdisplay: flex; justify-content: center;\n\t\t\t\tbackground-color: var(--gray-200);\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: var(--gray-400);\n\t\t\t\tmargin-top: .15rem;\n\t\t\t\tborder-radius: 6px;\n\t\t\t\theight: 100%;\n\t\t\t\tfont-size: 24px;\n\t\t\t}\n\t\t}\n\n\t\t.r-item-info {\n\t\t\tfont-size: 14px;\n\t\t\tpadding-right: 0;\n\t\t\tpadding-left: 10px;\n\t\t\twidth: 60%;\n\n\t\t\ta {\n\t\t\t\tcolor: var(--gray-800);\n\t\t\t\tfont-weight: 400;\n\t\t\t}\n\n\t\t\t.item-price {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tfont-weight: 600;\n\t\t\t\tcolor: var(--text-color);\n\t\t\t}\n\n\t\t\t.striked-item-price {\n\t\t\t\tfont-weight: 500;\n\t\t\t\tcolor: var(--gray-500);\n\t\t\t}\n\t\t}\n\t}\n}\n\n.product-code {\n\tpadding: .5rem 0;\n\tcolor: var(--text-muted);\n\tfont-size: 14px;\n\t.product-item-group {\n\t\tpadding-right: .25rem;\n\t\tborder-right: solid 1px var(--text-muted);\n\t}\n\n\t.product-item-code {\n\t\tpadding-left: .5rem;\n\t}\n}\n\n.item-configurator-dialog {\n\t.modal-body {\n\t\tpadding-bottom: var(--padding-xl);\n\n\t\t.status-area {\n\t\t\t.alert {\n\t\t\t\tpadding: var(--padding-xs) var(--padding-sm);\n\t\t\t\tfont-size: var(--text-sm);\n\t\t\t}\n\t\t}\n\n\t\t.form-layout {\n\t\t\tmax-height: 50vh;\n\t\t\toverflow-y: auto;\n\t\t}\n\n\t\t.section-body {\n\t\t\t.form-column {\n\t\t\t\t.form-group {\n\t\t\t\t\t.control-label {\n\t\t\t\t\t\tfont-size: var(--text-md);\n\t\t\t\t\t\tcolor: var(--gray-700);\n\t\t\t\t\t}\n\n\t\t\t\t\t.help-box {\n\t\t\t\t\t\tmargin-top: 2px;\n\t\t\t\t\t\tfont-size: var(--text-sm);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n.item-group-slideshow {\n\n\t.carousel-inner.rounded-carousel {\n\t\tborder-radius: var(--card-border-radius);\n\t}\n}\n\n.sub-category-container {\n\tpadding-bottom: .5rem;\n\tmargin-bottom: 1.25rem;\n\tborder-bottom: 1px solid var(--table-border-color);\n\n\t.heading {\n\t\tcolor: var(--gray-500);\n\t}\n}\n\n.scroll-categories {\n\t.category-pill {\n\t\tdisplay: inline-block;\n\t\twidth: fit-content;\n\t\tpadding: 6px 12px;\n\t\tmargin-bottom: 8px;\n\t\tbackground-color: #ecf5fe;\n\t\tfont-size: 14px;\n\t\tborder-radius: 18px;\n\t\tcolor: var(--blue-500);\n\t}\n}\n\n\n.shopping-badge {\n\tposition: relative;\n\ttop: -10px;\n\tleft: -12px;\n\tbackground: var(--red-600);\n\talign-items: center;\n\theight: 16px;\n\tfont-size: 10px;\n\tborder-radius: 50%;\n}\n\n\n.cart-animate {\n\tanimation: wiggle 0.5s linear;\n}\n@keyframes wiggle {\n\t8%,\n\t41% {\n\t\ttransform: translateX(-10px);\n\t}\n\t25%,\n\t58% {\n\t\ttransform: translate(10px);\n\t}\n\t75% {\n\t\ttransform: translate(-5px);\n\t}\n\t92% {\n\t\ttransform: translate(5px);\n\t}\n\t0%,\n\t100% {\n\t\ttransform: translate(0);\n\t}\n}\n\n.total-discount {\n\tfont-size: 14px;\n\tcolor: var(--primary-color) !important;\n}\n\n#page-cart {\n\t.shopping-cart-header {\n\t\tfont-weight: bold;\n\t}\n\n\t.cart-container {\n\t\tcolor: var(--text-color);\n\n\t\t.frappe-card {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tjustify-content: space-between;\n\t\t\theight: fit-content;\n\t\t}\n\n\t\t.cart-items-header {\n\t\t\tfont-weight: 600;\n\t\t}\n\n\t\t.cart-table {\n\t\t\ttr {\n\t\t\t\tmargin-bottom: 1rem;\n\t\t\t}\n\n\t\t\tth, tr, td {\n\t\t\t\tborder-color: var(--border-color);\n\t\t\t\tborder-width: 1px;\n\t\t\t}\n\n\t\t\tth {\n\t\t\t\tfont-weight: normal;\n\t\t\t\tfont-size: 13px;\n\t\t\t\tcolor: var(--text-muted);\n\t\t\t\tpadding: var(--padding-sm) 0;\n\t\t\t}\n\n\t\t\ttd {\n\t\t\t\tpadding: var(--padding-sm) 0;\n\t\t\t\tcolor: var(--text-color);\n\t\t\t}\n\n\t\t\t.cart-item-image {\n\t\t\t\twidth: 20%;\n\t\t\t\tmin-width: 100px;\n\t\t\t\timg {\n\t\t\t\t\tmax-height: 112px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.cart-items {\n\t\t\t\t.item-title {\n\t\t\t\t\twidth: 80%;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tcolor: var(--text-color);\n\t\t\t\t}\n\n\t\t\t\t.item-subtitle {\n\t\t\t\t\tcolor: var(--text-muted);\n\t\t\t\t\tfont-size: 13px;\n\t\t\t\t}\n\n\t\t\t\t.item-subtotal {\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t}\n\n\t\t\t\t.sm-item-subtotal {\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tdisplay: none;\n\n\t\t\t\t\t@media (max-width: 992px) {\n\t\t\t\t\t\tdisplay: unset !important;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.item-rate {\n\t\t\t\t\tfont-size: 13px;\n\t\t\t\t\tcolor: var(--text-muted);\n\t\t\t\t}\n\n\t\t\t\t.free-tag {\n\t\t\t\t\tpadding: 4px 8px;\n\t\t\t\t\tborder-radius: 4px;\n\t\t\t\t\tbackground-color: var(--dark-green-50);\n\t\t\t\t}\n\n\t\t\t\ttextarea {\n\t\t\t\t\twidth: 70%;\n\t\t\t\t\theight: 30px;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t.cart-tax-items {\n\t\t\t\t.item-grand-total {\n\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\tcolor: var(--text-color);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.column-sm-view {\n\t\t\t\t@media (max-width: 992px) {\n\t\t\t\t\tdisplay: none !important;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.item-column {\n\t\t\t\twidth: 50%;\n\t\t\t\t@media (max-width: 992px) {\n\t\t\t\t\twidth: 70%;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.remove-cart-item {\n\t\t\t\tborder-radius: 6px;\n\t\t\t\tborder: 1px solid var(--gray-100);\n\t\t\t\twidth: 28px;\n\t\t\t\theight: 28px;\n\t\t\t\tfont-weight: 300;\n\t\t\t\tcolor: var(--gray-700);\n\t\t\t\tbackground-color: var(--gray-100);\n\t\t\t\tfloat: right;\n\t\t\t\tcursor: pointer;\n\t\t\t\tmargin-top: .25rem;\n\t\t\t\tjustify-content: center;\n\t\t\t}\n\n\t\t\t.remove-cart-item-logo {\n\t\t\t\tmargin-top: 2px;\n\t\t\t\tmargin-left: 2.2px;\n\t\t\t\tfill: var(--gray-700) !important;\n\t\t\t}\n\t\t}\n\n\t\t.cart-payment-addresses {\n\t\t\thr {\n\t\t\t\tborder-color: var(--border-color);\n\t\t\t}\n\t\t}\n\n\t\t.payment-summary {\n\t\t\th6 {\n\t\t\t\tpadding-bottom: 1rem;\n\t\t\t\tborder-bottom: solid 1px var(--gray-200);\n\t\t\t}\n\n\t\t\ttable {\n\t\t\t\tfont-size: 14px;\n\t\t\t\ttd {\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tpadding-top: 0.35rem !important;\n\t\t\t\t\tborder: none !important;\n\t\t\t\t}\n\n\t\t\t\t&.grand-total {\n\t\t\t\t\tborder-top: solid 1px var(--gray-200);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.bill-label {\n\t\t\t\tcolor: var(--gray-600);\n\t\t\t}\n\n\t\t\t.bill-content {\n\t\t\t\tfont-weight: 500;\n\t\t\t\t&.net-total {\n\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\tfont-weight: 600;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.btn-coupon-code {\n\t\t\t\tfont-size: 14px;\n\t\t\t\tborder: dashed 1px var(--gray-400);\n\t\t\t\tbox-shadow: none;\n\t\t\t}\n\t\t}\n\n\t\t.number-spinner {\n\t\t\twidth: 75%;\n\t\t\tmin-width: 105px;\n\t\t\t.cart-btn {\n\t\t\t\tborder: none;\n\t\t\t\tbackground: var(--gray-100);\n\t\t\t\tbox-shadow: none;\n\t\t\t\twidth: 24px;\n\t\t\t\theight: 28px;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\tdisplay: flex;\n\t\t\t\tfont-size: 20px;\n\t\t\t\tfont-weight: 300;\n\t\t\t\tcolor: var(--gray-700);\n\t\t\t}\n\n\t\t\t.cart-qty {\n\t\t\t\theight: 28px;\n\t\t\t\tfont-size: 13px;\n\t\t\t\t&:disabled {\n\t\t\t\t\tbackground: var(--gray-100);\n\t\t\t\t\topacity: 0.65;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t.place-order-container {\n\t\t\t.btn-place-order {\n\t\t\t\tfloat: right;\n\t\t\t}\n\t\t}\n\t}\n\n\t.t-and-c-container {\n\t\tpadding: 1.5rem;\n\t}\n\n\t.t-and-c-terms {\n\t\tfont-size: 14px;\n\t}\n}\n\n.no-image-cart-item {\n\tmax-height: 112px;\n\tdisplay: flex; justify-content: center;\n\tbackground-color: var(--gray-200);\n\talign-items: center;\n\tcolor: var(--gray-400);\n\tmargin-top: .15rem;\n\tborder-radius: 6px;\n\theight: 100%;\n\tfont-size: 24px;\n}\n\n.cart-empty.frappe-card {\n\tmin-height: 76vh;\n\t@include flex(flex, center, center, column);\n\n\t.cart-empty-message {\n\t\tfont-size: 18px;\n\t\tcolor: var(--text-color);\n\t\tfont-weight: bold;\n\t}\n}\n\n.address-card {\n\t.card-title {\n\t\tfont-size: 14px;\n\t\tfont-weight: 500;\n\t}\n\n\t.card-body {\n\t\tmax-width: 80%;\n\t}\n\n\t.card-text {\n\t\tfont-size: 13px;\n\t\tcolor: var(--gray-700);\n\t}\n\n\t.card-link {\n\t\tfont-size: 13px;\n\n\t\tsvg use {\n\t\t\tstroke: var(--primary-color);\n\t\t}\n\t}\n\n\t.btn-change-address {\n\t\tborder: 1px solid var(--primary-color);\n\t\tcolor: var(--primary-color);\n\t\tbox-shadow: none;\n\t}\n}\n\n.address-header {\n\tmargin-top: .15rem;padding: 0;\n}\n\n.btn-new-address {\n\tfloat: right;\n\tfont-size: 15px !important;\n\tcolor: var(--primary-color) !important;\n}\n\n.btn-new-address:hover, .btn-change-address:hover {\n\tcolor: var(--primary-color) !important;\n}\n\n.modal .address-card {\n\t.card-body {\n\t\tpadding: var(--padding-sm);\n\t\tborder-radius: var(--border-radius);\n\t\tborder: 1px solid var(--dark-border-color);\n\t}\n}\n\n.cart-indicator {\n\tposition: absolute;\n\ttext-align: center;\n\twidth: 22px;\n\theight: 22px;\n\tleft: calc(100% - 40px);\n\ttop: 22px;\n\n\tborder-radius: 66px;\n\tbox-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);\n\tbackground: white;\n\tcolor: var(--primary-color);\n\tfont-size: 14px;\n\n\t&.list-indicator {\n\t\tposition: unset;\n\t\tmargin-left: auto;\n\t}\n}\n\n\n.like-action {\n\tvisibility: hidden;\n\ttext-align: center;\n\tposition: absolute;\n\tcursor: pointer;\n\twidth: 28px;\n\theight: 28px;\n\tleft: 20px;\n\ttop: 20px;\n\n\t/* White */\n\tbackground: white;\n\tbox-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);\n\tborder-radius: 66px;\n\n\t&.like-action-wished {\n\t\tvisibility: visible !important;\n\t}\n\n\t@media (max-width: 992px) {\n\t\tvisibility: visible !important;\n\t}\n}\n\n.like-action-list {\n\tvisibility: hidden;\n\ttext-align: center;\n\tposition: absolute;\n\tcursor: pointer;\n\twidth: 28px;\n\theight: 28px;\n\tleft: 20px;\n\ttop: 0;\n\n\t/* White */\n\tbackground: white;\n\tbox-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);\n\tborder-radius: 66px;\n\n\t&.like-action-wished {\n\t\tvisibility: visible !important;\n\t}\n\n\t@media (max-width: 992px) {\n\t\tvisibility: visible !important;\n\t}\n}\n\n.like-action-item-fp {\n\tvisibility: visible !important;\n\tposition: unset;\n\tfloat: right;\n}\n\n.like-animate {\n\tanimation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1;\n}\n\n@keyframes expand {\n\t30% {\n\t  transform: scale(1.3);\n\t}\n\t50% {\n\t  transform: scale(0.8);\n\t}\n\t70% {\n\t\ttransform: scale(1.1);\n\t}\n\t100% {\n\t  transform: scale(1);\n\t}\n  }\n\n.not-wished {\n\tcursor: pointer;\n\t--icon-stroke: #F47A7A !important;\n\n\t&:hover {\n\t\tfill: #F47A7A;\n\t}\n}\n\n.wished {\n\t--icon-stroke: none;\n\tfill: #F47A7A !important;\n}\n\n.list-row-checkbox {\n\t&:before {\n\t\tdisplay: none;\n\t}\n\n\t&:checked:before {\n\t\tdisplay: block;\n\t\tz-index: 1;\n\t}\n}\n\n#pay-for-order {\n\tpadding: .5rem 1rem; // Pay button in SO\n}\n\n.btn-explore-variants {\n\tvisibility: hidden;\n\tbox-shadow: none;\n\tmargin: var(--margin-sm) 0;\n\twidth: 90px;\n\tmax-height: 50px; // to avoid resizing on window resize\n\tflex: none;\n\ttransition: 0.3s ease;\n\n\tcolor: white;\n\tbackground-color: var(--orange-500);\n\tborder: 1px solid var(--orange-500);\n\tfont-size: 13px;\n\n\t&:hover {\n\t\tcolor: white;\n\t}\n}\n\n.btn-add-to-cart-list{\n\tvisibility: hidden;\n\tbox-shadow: none;\n\tmargin: var(--margin-sm) 0;\n\t// margin-top: auto !important;\n\tmax-height: 50px; // to avoid resizing on window resize\n\tflex: none;\n\ttransition: 0.3s ease;\n\n\tfont-size: 13px;\n\n\t&:hover {\n\t\tcolor: white;\n\t}\n\n\t@media (max-width: 992px) {\n\t\tvisibility: visible !important;\n\t}\n}\n\n.go-to-cart-grid {\n\tmax-height: 30px;\n\tmargin-top: 1rem !important;\n}\n\n.go-to-cart {\n\tmax-height: 30px;\n\tfloat: right;\n}\n\n.remove-wish {\n\tbackground-color: white;\n\tposition: absolute;\n\tcursor: pointer;\n\ttop:10px;\n\tright: 20px;\n\twidth: 32px;\n\theight: 32px;\n\n\tborder-radius: 50%;\n\tborder: 1px solid var(--gray-100);\n\tbox-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);\n}\n\n.wish-removed {\n\tdisplay: none;\n}\n\n.item-website-specification {\n\tfont-size: .875rem;\n\t.product-title {\n\t\tfont-size: 18px;\n\t}\n\n\t.table {\n\t\twidth: 70%;\n\t}\n\n\ttd {\n\t\tborder: none !important;\n\t}\n\n\t.spec-label {\n\t\tcolor: var(--gray-600);\n\t}\n\n\t.spec-content {\n\t\tcolor: var(--gray-800);\n\t}\n}\n\n.reviews-full-page {\n\tpadding: 1rem 2rem;\n}\n\n.ratings-reviews-section {\n\tborder-top: 1px solid #E2E6E9;\n\tpadding: .5rem 1rem;\n}\n\n.reviews-header {\n\tfont-size: 20px;\n\tfont-weight: 600;\n\tcolor: var(--gray-800);\n\tdisplay: flex;\n\talign-items: center;\n\tpadding: 0;\n}\n\n.btn-write-review {\n\tfloat: right;\n\tpadding: .5rem 1rem;\n\tfont-size: 14px;\n\tfont-weight: 400;\n\tborder: none !important;\n\tbox-shadow: none;\n\n\tcolor: var(--gray-900);\n\tbackground-color: var(--gray-100);\n\n\t&:hover {\n\t\tbox-shadow: var(--btn-shadow);\n\t}\n}\n\n.btn-view-more {\n\tfont-size: 14px;\n}\n\n.rating-summary-section {\n\tdisplay: flex;\n}\n\n.rating-summary-title {\n\tmargin-top: 0.15rem;\n\tfont-size: 18px;\n}\n\n.rating-summary-numbers {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\n\tborder-right: solid 1px var(--gray-100);\n}\n\n.user-review-title {\n\tmargin-top: 0.15rem;\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n.rating {\n\t--star-fill: var(--gray-300);\n\t.star-hover {\n\t\t--star-fill: var(--yellow-100);\n\t}\n\t.star-click {\n\t\t--star-fill: var(--yellow-300);\n\t}\n}\n\n.ratings-pill {\n\tbackground-color: var(--gray-100);\n\tpadding: .5rem 1rem;\n\tborder-radius: 66px;\n}\n\n.review {\n\tmax-width: 80%;\n\tline-height: 1.6;\n\tpadding-bottom: 0.5rem;\n\tborder-bottom: 1px solid #E2E6E9;\n}\n\n.review-signature {\n\tdisplay: flex;\n\tfont-size: 13px;\n\tcolor: var(--gray-500);\n\tfont-weight: 400;\n\n\t.reviewer {\n\t\tpadding-right: 8px;\n\t\tcolor: var(--gray-600);\n\t}\n}\n\n.rating-progress-bar-section {\n\tpadding-bottom: 2rem;\n\n\t.rating-bar-title {\n\t\tmargin-left: -15px;\n\t}\n\n\t.rating-progress-bar {\n\t\tmargin-bottom: 4px;\n\t\theight: 7px;\n\t\tmargin-top: 6px;\n\n\t\t.progress-bar-cosmetic {\n\t\t\tbackground-color: var(--gray-600);\n\t\t\tborder-radius: var(--border-radius);\n\t\t}\n\t}\n}\n\n.offer-container {\n\tfont-size: 14px;\n}\n\n#search-results-container {\n\tborder: 1px solid var(--gray-200);\n\tpadding: .25rem 1rem;\n\n\t.category-chip {\n\t\tbackground-color: var(--gray-100);\n\t\tborder: none !important;\n\t\tbox-shadow: none;\n\t}\n\n\t.recent-search {\n\t\tpadding: .5rem .5rem;\n\t\tborder-radius: var(--border-radius);\n\n\t\t&:hover {\n\t\t\tbackground-color: var(--gray-100);\n\t\t}\n\t}\n}\n\n#search-box {\n\tbackground-color: white;\n\theight: 100%;\n\tpadding-left: 2.5rem;\n\tborder: 1px solid var(--gray-200);\n}\n\n.search-icon {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\twidth: 2.5rem;\n\theight: 100%;\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tpadding-bottom: 1px;\n}\n\n#toggle-view {\n\tfloat: right;\n\n\t.btn-primary {\n\t\tbackground-color: var(--gray-600);\n\t\tbox-shadow: 0 0 0 0.2rem var(--gray-400);\n\t}\n}\n\n.placeholder-div {\n\theight:80%;\n\twidth: -webkit-fill-available;\n\tpadding: 50px;\n\ttext-align: center;\n\tbackground-color: #F9FAFA;\n\tborder-top-left-radius: calc(0.75rem - 1px);\n\tborder-top-right-radius: calc(0.75rem - 1px);\n}\n.placeholder {\n\tfont-size: 72px;\n}\n\n[data-path=\"cart\"] {\n\t.modal-backdrop {\n\t\tbackground-color: var(--gray-50); // lighter backdrop only on cart freeze\n\t}\n}\n\n.item-thumb {\n\theight: 50px;\n\tmax-width: 80px;\n\tmin-width: 80px;\n\tobject-fit: cover;\n}\n\n.brand-line {\n\tcolor: gray;\n}\n\n.btn-next, .btn-prev {\n\tfont-size: 14px;\n}\n\n.alert-error {\n\tcolor: #e27a84;\n\tbackground-color: #fff6f7;\n\tborder-color: #f5c6cb;\n}\n\n.font-md {\n\tfont-size: 14px !important;\n}\n\n.in-green {\n\tcolor: var(--green-info) !important;\n\tfont-weight: 500;\n}\n\n.has-stock {\n\tfont-weight: 400 !important;\n}\n\n.out-of-stock {\n\tfont-weight: 400;\n\tfont-size: 14px;\n\tline-height: 20px;\n\tcolor: #F47A7A;\n}\n\n.mt-minus-2 {\n\tmargin-top: -2rem;\n}\n\n.mt-minus-1 {\n\tmargin-top: -1rem;\n}\n\n.tooltip-content {\n\tposition: absolute;\n\tbottom: 100%;\n\tleft: 0;\n\tz-index: 9999;\n\tpadding: 2px 6px;\n\tborder-radius: var(--border-radius-sm);\n\tbackground-color: var(--bg-dark-gray);\n\tcolor: var(--text-dark);\n\tfont-size: var(--text-xs);\n\topacity: 0;\n\tcursor: copy;\n\ttransition: opacity 0.3s, transform 3s;\n\tpointer-events: none;\n}\n\n.show-tooltip .frappe-control:hover .tooltip-content {\n\topacity: 1;\n\ttransform: translate3d(0, 0, 0);\n\tpointer-events: auto;\n}\n\n.w-fit {\n\twidth: fit-content !important;\n}"
  },
  {
    "path": "webshop/public/web.bundle.js",
    "content": "import './js/init'\n\nimport './js/customer_reviews'\nimport './js/product_ui/grid'\nimport './js/product_ui/list'\nimport './js/product_ui/search'\nimport './js/product_ui/views'\nimport './js/shopping_cart'\nimport './js/wishlist'\n"
  },
  {
    "path": "webshop/setup/install.py",
    "content": "import click\nimport frappe\n\nfrom frappe import _\nfrom frappe.custom.doctype.custom_field.custom_field import create_custom_fields\n\nfrom webshop.webshop.utils.setup import has_ecommerce_fields\n\ndef after_install():\n\trun_patches()\n\tcopy_from_ecommerce_settings()\n\tdrop_ecommerce_settings()\n\tremove_ecommerce_settings_doctype()\n\tadd_custom_fields()\n\tnavbar_add_products_link()\n\tsay_thanks()\n\n\ndef copy_from_ecommerce_settings():\n\tif not has_ecommerce_fields():\n\t\treturn\n\n\tfrappe.reload_doc(\"webshop\", \"doctype\", \"webshop_settings\")\n\n\tqb = frappe.qb\n\ttable = frappe.qb.Table(\"tabSingles\")\n\told_doctype = \"E Commerce Settings\"\n\tnew_doctype = \"Webshop Settings\"\n\n\tentries = (\n\t\tqb.from_(table)\n\t\t.select(table.field, table.value)\n\t\t.where((table.doctype == old_doctype) & (table.field != \"name\"))\n\t\t.run(as_dict=True)\n\t)\n\n\tfor e in entries:\n\t\tqb.into(table).insert(new_doctype, e.field, e.value).run()\n\n\tfor doctype in [\"Website Filter Field\", \"Website Attribute\"]:\n\t\ttable = qb.DocType(doctype)\n\t\tquery = (\n\t\t\tqb.update(table)\n\t\t\t.set(table.parent, new_doctype)\n\t\t\t.set(table.parenttype, new_doctype)\n\t\t\t.where(table.parent == old_doctype)\n\t\t)\n\n\t\tquery.run()\n\ndef drop_ecommerce_settings():\n\tfrappe.delete_doc_if_exists(\"DocType\", \"E Commerce Settings\", force=True)\n\n\ndef remove_ecommerce_settings_doctype():\n\tif not has_ecommerce_fields():\n\t\treturn\n\n\ttable = frappe.qb.Table(\"tabSingles\")\n\told_doctype = \"E Commerce Settings\"\n\n\tfrappe.qb.from_(table).delete().where(table.doctype == old_doctype).run()\n\n\ndef add_custom_fields():\n\tcustom_fields = {\n\t\t\"Item\": [\n\t\t\t{\n\t\t\t\t\"default\": 0,\n\t\t\t\t\"depends_on\": \"published_in_website\",\n\t\t\t\t\"fieldname\": \"published_in_website\",\n\t\t\t\t\"fieldtype\": \"Check\",\n\t\t\t\t\"ignore_user_permissions\": 1,\n\t\t\t\t\"insert_after\": \"default_manufacturer_part_no\",\n\t\t\t\t\"label\": \"Published In Website\",\n\t\t\t\t\"read_only\": 1,\n\t\t\t\t\"no_copy\": 1,\n\t\t\t}\n\t\t],\n\t\t\"Item Group\": [\n\t\t\t{\n\t\t\t\t\"fieldname\": \"custom_website_settings\",\n\t\t\t\t\"fieldtype\": \"Section Break\",\n\t\t\t\t\"label\": \"Website Settings\",\n\t\t\t\t\"insert_after\": \"taxes\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"default\": \"0\",\n\t\t\t\t\"description\": \"Make Item Group visible in website\",\n\t\t\t\t\"fieldname\": \"show_in_website\",\n\t\t\t\t\"fieldtype\": \"Check\",\n\t\t\t\t\"label\": \"Show in Website\",\n\t\t\t\t\"insert_after\": \"custom_website_settings\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"fieldname\": \"route\",\n\t\t\t\t\"fieldtype\": \"Data\",\n\t\t\t\t\"label\": \"Route\",\n\t\t\t\t\"no_copy\": 1,\n\t\t\t\t\"unique\": 1,\n\t\t\t\t\"insert_after\": \"show_in_website\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"fieldname\": \"website_title\",\n\t\t\t\t\"fieldtype\": \"Data\",\n\t\t\t\t\"label\": \"Title\",\n\t\t\t\t\"insert_after\": \"route\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"description\": \"HTML / Banner that will show on the top of product list.\",\n\t\t\t\t\"fieldname\": \"description\",\n\t\t\t\t\"fieldtype\": \"Text Editor\",\n\t\t\t\t\"label\": \"Description\",\n\t\t\t\t\"insert_after\": \"website_title\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"default\": \"0\",\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"description\": \"Include Website Items belonging to child Item Groups\",\n\t\t\t\t\"fieldname\": \"include_descendants\",\n\t\t\t\t\"fieldtype\": \"Check\",\n\t\t\t\t\"label\": \"Include Descendants\",\n\t\t\t\t\"insert_after\": \"website_title\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fieldname\": \"column_break_16\",\n\t\t\t\t\"fieldtype\": \"Column Break\",\n\t\t\t\t\"insert_after\": \"include_descendants\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"fieldname\": \"weightage\",\n\t\t\t\t\"fieldtype\": \"Int\",\n\t\t\t\t\"label\": \"Weightage\",\n\t\t\t\t\"insert_after\": \"column_break_16\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"description\": \"Show this slideshow at the top of the page\",\n\t\t\t\t\"fieldname\": \"slideshow\",\n\t\t\t\t\"fieldtype\": \"Link\",\n\t\t\t\t\"label\": \"Slideshow\",\n\t\t\t\t\"options\": \"Website Slideshow\",\n\t\t\t\t\"insert_after\": \"weightage\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"fieldname\": \"website_specifications\",\n\t\t\t\t\"fieldtype\": \"Table\",\n\t\t\t\t\"label\": \"Website Specifications\",\n\t\t\t\t\"options\": \"Item Website Specification\",\n\t\t\t\t\"insert_after\": \"description\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"collapsible\": 1,\n\t\t\t\t\"depends_on\": \"show_in_website\",\n\t\t\t\t\"fieldname\": \"website_filters_section\",\n\t\t\t\t\"fieldtype\": \"Section Break\",\n\t\t\t\t\"label\": \"Website Filters\",\n\t\t\t\t\"insert_after\": \"website_specifications\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fieldname\": \"filter_fields\",\n\t\t\t\t\"fieldtype\": \"Table\",\n\t\t\t\t\"label\": \"Item Fields\",\n\t\t\t\t\"options\": \"Website Filter Field\",\n\t\t\t\t\"insert_after\": \"website_filters_section\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"fieldname\": \"filter_attributes\",\n\t\t\t\t\"fieldtype\": \"Table\",\n\t\t\t\t\"label\": \"Attributes\",\n\t\t\t\t\"options\": \"Website Attribute\",\n\t\t\t\t\"insert_after\": \"filter_fields\",\n\t\t\t},\n\t\t]\n\t}\n\n\tfrappe.make_property_setter(\n\t\t{\n\t\t\t\"doctype\": \"Item Group\",\n\t\t\t\"doctype_or_field\": \"DocType\",\n\t\t\t\"fieldname\": \"allow_guest_to_view\",\n\t\t\t\"property\": \"allow_guest_to_view\",\n\t\t\t\"value\": 1,\n\t\t\t\"property_type\": \"Check\"\n\t\t},\n\t\tis_system_generated=True,\n\t)\n\n\treturn create_custom_fields(custom_fields)\n\ndef navbar_add_products_link():\n\twebsite_settings = frappe.get_doc(\"Website Settings\")\n\tif website_settings.top_bar_items:\n\t\treturn\n\n\twebsite_settings.append(\n\t\t\"top_bar_items\",\n\t\t{\n\t\t\t\"label\": _(\"Products\"),\n\t\t\t\"url\": \"/all-products\",\n\t\t\t\"right\": False,\n\t\t},\n\t)\n\n\twebsite_settings.save()\n\n\ndef say_thanks():\n\tclick.secho(\"Thank you for installing Frappe Webshop!\", color=\"green\")\n\n\npatches = [\n\t\"create_website_items\",\n\t\"populate_e_commerce_settings\",\n\t\"add_homepage_field\",\n\t\"make_homepage_products_website_items\",\n\t\"fetch_thumbnail_in_website_items\",\n\t\"convert_to_website_item_in_item_card_group_template\",\n\t\"shopping_cart_to_ecommerce\",\n\t\"copy_custom_field_filters_to_website_item\",\n]\n\ndef run_patches():\n\t# Customers migrating from v13 to v15 directly need to run all below patches\n\n\tfrappe.flags.in_patch = True\n\n\ttry:\n\t\tfor patch in patches:\n\t\t\tfrappe.get_attr(f\"webshop.patches.{patch}.execute\")()\n\n\tfinally:\n\t\tfrappe.flags.in_patch = False\n\n\n"
  },
  {
    "path": "webshop/templates/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/templates/generators/item/item.html",
    "content": "{% extends \"templates/web.html\" %}\n{% from \"webshop/templates/includes/macros.html\" import recommended_item_row %}\n\n{% block title %} {{ title }} {% endblock %}\n\n{% block breadcrumbs %}\n<div class=\"item-breadcrumbs small text-muted\">\n\t{% include \"templates/includes/breadcrumbs.html\" %}\n</div>\n{% endblock %}\n\n{% block page_content %}\n<div class=\"product-container item-main\">\n\t{% from \"webshop/templates/includes/macros.html\" import product_image %}\n\t<div class=\"item-content\">\n\t\t<div class=\"product-page-content\" itemscope itemtype=\"http://schema.org/Product\">\n\t\t\t<!-- Image, Description, Add to Cart -->\n\t\t\t<div class=\"row mb-5\">\n\t\t\t\t{% include \"templates/generators/item/item_image.html\" %}\n\t\t\t\t{% include \"templates/generators/item/item_details.html\" %}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n\n<!-- Additional Info/Reviews, Recommendations -->\n<div class=\"d-flex\">\n\t{% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %}\n\t{% set info_col = 'col-9' if show_recommended_items else 'col-12' %}\n\n\t{% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %}\n\n\t<div class=\"product-container mt-4 {{ padding_top }} {{ info_col }}\">\n\t\t<div class=\"item-content {{ 'mt-minus-2' if (show_tabs and tabs) else '' }}\">\n\t\t\t<div class=\"product-page-content\" itemscope itemtype=\"http://schema.org/Product\">\n\t\t\t\t<!-- Product Specifications Table Section -->\n\t\t\t\t{% if show_tabs and tabs %}\n\t\t\t\t\t<div class=\"category-tabs\">\n\t\t\t\t\t\t<!-- tabs -->\n\t\t\t\t\t\t\t{{ web_block(\"Section with Tabs\", values=tabs, add_container=0,\n\t\t\t\t\t\t\t\tadd_top_padding=0, add_bottom_padding=0)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t</div>\n\t\t\t\t{% elif website_specifications %}\n\t\t\t\t\t{% include \"templates/generators/item/item_specifications.html\"%}\n\t\t\t\t{% endif %}\n\n\t\t\t\t<!-- Advanced Custom Website Content -->\n\t\t\t\t{{ doc.website_content or '' }}\n\n\t\t\t\t<!-- Reviews and Comments -->\n\t\t\t\t{% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %}\n\t\t\t\t\t{% include \"templates/generators/item/item_reviews.html\"%}\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<!-- Recommended Items -->\n\t{% if show_recommended_items %}\n\t\t<div class=\"mt-4 col-3 recommended-item-section\">\n\t\t\t<span class=\"recommendation-header\">{{ _(\"Recommended\") }}</span>\n\t\t\t<div class=\"product-container mt-2 recommendation-container\">\n\t\t\t\t{% for item in recommended_items %}\n\t\t\t\t\t{{ recommended_item_row(item) }}\n\t\t\t\t{% endfor %}\n\t\t\t</div>\n\t\t</div>\n\t{% endif %}\n\n</div>\n{% endblock %}\n\n{% block base_scripts %}\n<!-- js should be loaded in body! -->\n<script type=\"text/javascript\" src=\"/assets/frappe/js/lib/jquery/jquery.min.js\"></script>\n{{ include_script(\"frappe-web.bundle.js\") }}\n{{ include_script(\"controls.bundle.js\") }}\n{{ include_script(\"dialog.bundle.js\") }}\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/generators/item/item_add_to_cart.html",
    "content": "{% if shopping_cart and shopping_cart.cart_settings.enabled %}\n\n{% set cart_settings = shopping_cart.cart_settings %}\n{% set product_info = shopping_cart.product_info %}\n\n<div class=\"item-cart row mt-2\" data-variant-item-code=\"{{ item_code }}\">\n\t<div class=\"col-md-12\">\n\t\t<!-- Price and Availability -->\n\t\t{% if cart_settings.show_price and product_info.price %}\n\t\t\t{% set price_info = product_info.price %}\n\n\t\t\t<div class=\"product-price\" itemprop=\"offers\" itemscope itemtype=\"https://schema.org/AggregateOffer\">\n\t\t\t\t<!-- Final Price -->\n\t\t\t\t{% if price_info.formatted_mrp %}\n\t\t\t\t\t<span itemprop=\"lowPrice\" content=\"{{ price_info.formatted_price_sales_uom }}\">{{ price_info.formatted_price_sales_uom }}</span>\n\t\t\t\t{% else %}\n\t\t\t\t\t<span itemprop=\"highPrice\" content=\"{{ price_info.formatted_price_sales_uom }}\">{{ price_info.formatted_price_sales_uom }}</span>\n\t\t\t\t{% endif %}\n\n\t\t\t\t<!-- Striked Price and Discount  -->\n\t\t\t\t{% if price_info.formatted_mrp %}\n\t\t\t\t\t<small itemprop=\"highPrice\" class=\"formatted-price\">\n\t\t\t\t\t\t<s>MRP {{ price_info.formatted_mrp }}</s>\n\t\t\t\t\t</small>\n\t\t\t\t\t<small class=\"ml-1 formatted-price in-green\">\n\t\t\t\t\t\t-{{ price_info.get(\"formatted_discount_percent\") or price_info.get(\"formatted_discount_rate\")}}\n\t\t\t\t\t</small>\n\t\t\t\t{% endif %}\n\n\t\t\t\t<!-- Price per UOM -->\n\t\t\t\t<small class=\"formatted-price ml-2\">\n\t\t\t\t\t({{ price_info.formatted_price }} / {{ product_info.uom }})\n\t\t\t\t</small>\n\t\t\t</div>\n\t\t{% else %}\n\t\t\t{{ _(\"UOM\") }} : {{ product_info.uom }}\n\t\t{% endif %}\n\n\t\t{% if cart_settings.show_stock_availability %}\n\t\t<div class=\"mt-2\">\n\t\t\t{% if product_info.get(\"on_backorder\") %}\n\t\t\t\t<span class=\"no-stock out-of-stock\" style=\"color: var(--primary-color);\">\n\t\t\t\t\t{{ _('Available on backorder') }}\n\t\t\t\t</span>\n\t\t\t{% elif product_info.in_stock == 0 %}\n\t\t\t\t<span class=\"no-stock out-of-stock\">\n\t\t\t\t\t{{ _('Out of stock') }}\n\t\t\t\t</span>\n\t\t\t{% elif product_info.in_stock == 1 %}\n\t\t\t\t<span class=\"in-green has-stock\">\n\t\t\t\t\t{{ _('In stock') }}\n\t\t\t\t\t{% if product_info.show_stock_qty and product_info.stock_qty %}\n\t\t\t\t\t\t({{ product_info.stock_qty }})\n\t\t\t\t\t{% endif %}\n\t\t\t\t</span>\n\t\t\t{% endif %}\n\t\t</div>\n\t\t{% endif %}\n\n\t\t<!-- Offers -->\n\t\t{% if doc.offers %}\n\t\t\t<br>\n\t\t\t<div class=\"offers-heading mb-4\">\n\t\t\t\t<span class=\"mr-1 tag-icon\">\n\t\t\t\t\t<svg class=\"icon icon-lg\"><use href=\"#icon-tag\"></use></svg>\n\t\t\t\t</span>\n\t\t\t\t<b>Available Offers</b>\n\t\t\t</div>\n\t\t\t<div class=\"offer-container\">\n\t\t\t\t{% for offer in doc.offers %}\n\t\t\t\t<div class=\"mt-2 d-flex\">\n\t\t\t\t\t<div class=\"mr-2\" >\n\t\t\t\t\t\t<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke=\"var(--yellow-500)\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n\t\t\t\t\t\t\t<path d=\"M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t\t<path d=\"M15 9L9 15\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t\t<path d=\"M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z\" fill=\"white\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t\t<path d=\"M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z\" fill=\"white\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p class=\"mr-1 mb-1\">\n\t\t\t\t\t\t{{ _(offer.offer_title) }}:\n\t\t\t\t\t\t{{ _(offer.offer_subtitle) if offer.offer_subtitle else '' }}\n\t\t\t\t\t\t<a class=\"offer-details\" href=\"#\"\n\t\t\t\t\t\t\tdata-offer-title=\"{{ offer.offer_title }}\" data-offer-id=\"{{ offer.name }}\"\n\t\t\t\t\t\t\trole=\"button\">\n\t\t\t\t\t\t\t{{ _(\"More\") }}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t{% endfor %}\n\t\t\t</div>\n\t\t{% endif %}\n\n\t\t<!-- Add to Cart / View in Cart, Contact Us -->\n\t\t<div class=\"mt-6 mb-5\">\n\t\t\t<div class=\"mb-4 d-flex\">\n\t\t\t\t<!-- Add to Cart -->\n\t\t\t\t{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.get(\"in_stock\")) %}\n\t\t\t\t\t<a href=\"/cart\" class=\"btn btn-light btn-view-in-cart hidden mr-2 font-md\"\n\t\t\t\t\t\trole=\"button\">\n\t\t\t\t\t\t{{  _(\"View in Cart\") if cart_settings.enable_checkout else _(\"View in Quote\") }}\n\t\t\t\t\t</a>\n\t\t\t\t\t<button\n\t\t\t\t\t\tdata-item-code=\"{{item_code}}\"\n\t\t\t\t\t\tclass=\"btn btn-primary btn-add-to-cart mr-2 w-30-40\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<span class=\"mr-2\">\n\t\t\t\t\t\t\t<svg class=\"icon icon-md\">\n\t\t\t\t\t\t\t\t<use href=\"#icon-assets\"></use>\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{{ _(\"Add to Cart\") if cart_settings.enable_checkout else  _(\"Add to Quote\") }}\n\t\t\t\t\t</button>\n\t\t\t\t{% endif %}\n\n\t\t\t\t<!-- Contact Us -->\n\t\t\t\t{% if cart_settings.show_contact_us_button %}\n\t\t\t\t\t{% include \"templates/generators/item/item_inquiry.html\" %}\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n\n<script>\n\tfrappe.ready(() => {\n\t\t$('.page_content').on('click', '.btn-add-to-cart', (e) => {\n\t\t\t// Bind action on add to cart button\n\t\t\tconst $btn = $(e.currentTarget);\n\t\t\t$btn.prop('disabled', true);\n\t\t\tconst item_code = $btn.data('item-code');\n\t\t\twebshop.webshop.shopping_cart.update_cart({\n\t\t\t\titem_code,\n\t\t\t\tqty: 1,\n\t\t\t\tcallback(r) {\n\t\t\t\t\t$btn.prop('disabled', false);\n\t\t\t\t\tif (r.message) {\n\t\t\t\t\t\t$('.btn-add-to-cart, .btn-view-in-cart').toggleClass('hidden');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\t$('.page_content').on('click', '.offer-details', (e) => {\n\t\t\t// Bind action on More link in Offers\n\t\t\tconst $btn = $(e.currentTarget);\n\t\t\t$btn.prop('disabled', true);\n\n\t\t\tvar d = new frappe.ui.Dialog({\n\t\t\t\ttitle: __($btn.data('offer-title')),\n\t\t\t\tfields: [\n\t\t\t\t\t{\n\t\t\t\t\t\tfieldname: 'offer_details',\n\t\t\t\t\t\tfieldtype: 'HTML'\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfieldname: 'section_break',\n\t\t\t\t\t\tfieldtype: 'Section Break'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t});\n\n\t\t\tfrappe.call({\n\t\t\t\tmethod: 'webshop.webshop.doctype.website_offer.website_offer.get_offer_details',\n\t\t\t\targs: {\n\t\t\t\t\toffer_id: $btn.data('offer-id')\n\t\t\t\t},\n\t\t\t\tcallback: (value) => {\n\t\t\t\t\td.set_value(\"offer_details\", value.message);\n\t\t\t\t\td.show();\n\t\t\t\t\t$btn.prop('disabled', false);\n\t\t\t\t}\n\t\t\t})\n\n\t\t});\n\t});\n\n\n</script>\n\n{% endif %}\n"
  },
  {
    "path": "webshop/templates/generators/item/item_configure.html",
    "content": "{% if shopping_cart and shopping_cart.cart_settings.enabled %}\n{% set cart_settings = shopping_cart.cart_settings %}\n\n<div class=\"mt-5 mb-6\">\n\t{% if cart_settings.enable_variants | int %}\n\t<button class=\"btn btn-primary-light btn-configure font-md mr-2\"\n\t\tdata-item-code=\"{{ doc.item_code }}\"\n\t\tdata-item-name=\"{{ doc.web_item_name }}\"\n\t>\n\t\t{{ _('Select Variant') }}\n\t</button>\n\t{% endif %}\n\t{% if cart_settings.show_contact_us_button %}\n\t\t{% include \"templates/generators/item/item_inquiry.html\" %}\n\t{% endif %}\n</div>\n<script>\n{% include \"templates/generators/item/item_configure.js\" %}\n</script>\n{% endif %}\n"
  },
  {
    "path": "webshop/templates/generators/item/item_configure.js",
    "content": "class ItemConfigure {\n\tconstructor(item_code, item_name) {\n\t\tthis.item_code = item_code;\n\t\tthis.item_name = item_name;\n\n\t\tthis.get_attributes_and_values()\n\t\t\t.then(attribute_data => {\n\t\t\t\tthis.attribute_data = attribute_data;\n\t\t\t\tthis.show_configure_dialog();\n\t\t\t});\n\t}\n\n\tshow_configure_dialog() {\n\t\tconst fields = this.attribute_data.map(a => {\n\t\t\treturn {\n\t\t\t\tfieldtype: 'Select',\n\t\t\t\tlabel: a.attribute,\n\t\t\t\tfieldname: a.attribute,\n\t\t\t\toptions: a.values.map(v => {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tlabel: v,\n\t\t\t\t\t\tvalue: v\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t\tchange: (e) => {\n\t\t\t\t\tthis.on_attribute_selection(e);\n\t\t\t\t}\n\t\t\t};\n\t\t});\n\n\t\tthis.dialog = new frappe.ui.Dialog({\n\t\t\ttitle: __('Select Variant for {0}', [this.item_name]),\n\t\t\tfields,\n\t\t\ton_hide: () => {\n\t\t\t\tset_continue_configuration();\n\t\t\t}\n\t\t});\n\n\t\tthis.attribute_data.forEach(a => {\n\t\t\tconst field = this.dialog.get_field(a.attribute);\n\t\t\tconst $a = $(`<a href>${__(\"Clear\")}</a>`);\n\t\t\t$a.on('click', (e) => {\n\t\t\t\te.preventDefault();\n\t\t\t\tthis.dialog.set_value(a.attribute, '');\n\t\t\t});\n\t\t\tfield.$wrapper.find('.help-box').append($a);\n\t\t});\n\n\t\tthis.append_status_area();\n\t\tthis.dialog.show();\n\n\t\tthis.dialog.set_values(JSON.parse(localStorage.getItem(this.get_cache_key())));\n\n\t\t$('.btn-configure').prop('disabled', false);\n\t}\n\n\ton_attribute_selection(e) {\n\t\tif (e) {\n\t\t\tconst changed_fieldname = $(e.target).data('fieldname');\n\t\t\tthis.show_range_input_if_applicable(changed_fieldname);\n\t\t} else {\n\t\t\tthis.show_range_input_for_all_fields();\n\t\t}\n\n\t\tconst values = this.dialog.get_values();\n\t\tif (Object.keys(values).length === 0) {\n\t\t\tthis.clear_status();\n\t\t\tlocalStorage.removeItem(this.get_cache_key());\n\t\t\treturn;\n\t\t}\n\n\t\t// save state\n\t\tlocalStorage.setItem(this.get_cache_key(), JSON.stringify(values));\n\n\t\t// show\n\t\tthis.set_loading_status();\n\n\t\tthis.get_next_attribute_and_values(values)\n\t\t\t.then(data => {\n\t\t\t\tconst {\n\t\t\t\t\tvalid_options_for_attributes,\n\t\t\t\t} = data;\n\n\t\t\t\tthis.set_item_found_status(data);\n\n\t\t\t\tfor (let attribute in valid_options_for_attributes) {\n\t\t\t\t\tconst valid_options = valid_options_for_attributes[attribute];\n\t\t\t\t\tconst options = this.dialog.get_field(attribute).df.options;\n\t\t\t\t\tconst new_options = options.map(o => {\n\t\t\t\t\t\to.disabled = !valid_options.includes(o.value);\n\t\t\t\t\t\treturn o;\n\t\t\t\t\t});\n\n\t\t\t\t\tthis.dialog.set_df_property(attribute, 'options', new_options);\n\t\t\t\t\tthis.dialog.get_field(attribute).set_options();\n\t\t\t\t}\n\t\t\t});\n\t}\n\n\tshow_range_input_for_all_fields() {\n\t\tthis.dialog.fields.forEach(f => {\n\t\t\tif (![\"Section Break\", \"Coulmn Break\"].includes(f.fieldtype)) {\n\t\t\t\tthis.show_range_input_if_applicable(f.fieldname);\n\t\t\t}\n\t\t});\n\t}\n\n\tshow_range_input_if_applicable(fieldname) {\n\t\tconst changed_field = this.dialog.get_field(fieldname);\n\t\tconst changed_value = changed_field.get_value();\n\t\tif (changed_value && changed_value.includes(' to ')) {\n\t\t\t// possible range input\n\t\t\tlet numbers = changed_value.split(' to ');\n\t\t\tnumbers = numbers.map(number => parseFloat(number));\n\n\t\t\tif (!numbers.some(n => isNaN(n))) {\n\t\t\t\tnumbers.sort((a, b) => a - b);\n\t\t\t\tif (changed_field.$input_wrapper.find('.range-selector').length) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst parent = $('<div class=\"range-selector\">')\n\t\t\t\t\t.insertBefore(changed_field.$input_wrapper.find('.help-box'));\n\t\t\t\tconst control = frappe.ui.form.make_control({\n\t\t\t\t\tdf: {\n\t\t\t\t\t\tfieldtype: 'Int',\n\t\t\t\t\t\tlabel: __('Enter value betweeen {0} and {1}', [numbers[0], numbers[1]]),\n\t\t\t\t\t\tchange: () => {\n\t\t\t\t\t\t\tconst value = control.get_value();\n\t\t\t\t\t\t\tif (value < numbers[0] || value > numbers[1]) {\n\t\t\t\t\t\t\t\tcontrol.$wrapper.addClass('was-validated');\n\t\t\t\t\t\t\t\tcontrol.set_description(\n\t\t\t\t\t\t\t\t\t__('Value must be between {0} and {1}', [numbers[0], numbers[1]]));\n\t\t\t\t\t\t\t\tcontrol.$input[0].setCustomValidity('error');\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontrol.$wrapper.removeClass('was-validated');\n\t\t\t\t\t\t\t\tcontrol.set_description('');\n\t\t\t\t\t\t\t\tcontrol.$input[0].setCustomValidity('');\n\t\t\t\t\t\t\t\tthis.update_range_values(fieldname, value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\trender_input: true,\n\t\t\t\t\tparent\n\t\t\t\t});\n\t\t\t\tcontrol.$wrapper.addClass('mt-3');\n\t\t\t}\n\t\t}\n\t}\n\n\tupdate_range_values(attribute, range_value) {\n\t\tthis.range_values = this.range_values || {};\n\t\tthis.range_values[attribute] = range_value;\n\t}\n\n\tshow_remaining_optional_attributes() {\n\t\t// show all attributes if remaining\n\t\t// unselected attributes are all optional\n\t\tconst unselected_attributes = this.dialog.fields.filter(df => {\n\t\t\tconst value_selected = this.dialog.get_value(df.fieldname);\n\t\t\treturn !value_selected;\n\t\t});\n\t\tconst is_optional_attribute = df => {\n\t\t\tconst optional_attributes = this.attribute_data\n\t\t\t\t.filter(a => a.optional).map(a => a.attribute);\n\t\t\treturn optional_attributes.includes(df.fieldname);\n\t\t};\n\t\tif (unselected_attributes.every(is_optional_attribute)) {\n\t\t\tunselected_attributes.forEach(df => {\n\t\t\t\tthis.dialog.fields_dict[df.fieldname].$wrapper.show();\n\t\t\t});\n\t\t}\n\t}\n\n\tset_loading_status() {\n\t\tthis.dialog.$status_area.html(`\n\t\t\t<div class=\"alert alert-warning d-flex justify-content-between align-items-center\" role=\"alert\">\n\t\t\t\t${__('Loading...')}\n\t\t\t</div>\n\t\t`);\n\t}\n\n\tset_item_found_status(data) {\n\t\tconst html = this.get_html_for_item_found(data);\n\t\tthis.dialog.$status_area.html(html);\n\t}\n\n\tclear_status() {\n\t\tthis.dialog.$status_area.empty();\n\t}\n\n\tget_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) {\n\t\tconst one_item = exact_match.length === 1\n\t\t\t? exact_match[0]\n\t\t\t: filtered_items_count === 1\n\t\t\t\t? filtered_items[0]\n\t\t\t\t: '';\n\n\t\tconst item_add_to_cart = one_item ? `\n\t\t\t<button data-item-code=\"${one_item}\"\n\t\t\t\tclass=\"btn btn-primary btn-add-to-cart w-100\"\n\t\t\t\tdata-action=\"btn_add_to_cart\"\n\t\t\t>\n\t\t\t\t<span class=\"mr-2\">\n\t\t\t\t\t${frappe.utils.icon('assets', 'md')}\n\t\t\t\t</span>\n\t\t\t\t${__(\"Add to Cart\")}\n\t\t\t</button>\n\t\t` : '';\n\n\t\tconst items_found = filtered_items_count === 1 ?\n\t\t\t__('{0} item found.', [filtered_items_count]) :\n\t\t\t__('{0} items found.', [filtered_items_count]);\n\n\t\t/* eslint-disable indent */\n\t\tconst item_found_status = exact_match.length === 1\n\t\t\t? `<div class=\"alert alert-success d-flex justify-content-between align-items-center\" role=\"alert\">\n\t\t\t\t<div><div>\n\t\t\t\t\t${one_item}\n\t\t\t\t\t${product_info && product_info.price && !$.isEmptyObject(product_info.price)\n\t\t\t\t\t\t? '(' + product_info.price.formatted_price_sales_uom + ')'\n\t\t\t\t\t\t: ''\n\t\t\t\t\t}\n\t\t\t\t</div></div>\n\t\t\t\t<a href data-action=\"btn_clear_values\" data-item-code=\"${one_item}\">\n\t\t\t\t\t${__('Clear Values')}\n\t\t\t\t</a>\n\t\t\t</div>`\n\t\t\t: `<div class=\"alert alert-warning d-flex justify-content-between align-items-center\" role=\"alert\">\n\t\t\t\t\t<span>\n\t\t\t\t\t\t${items_found}\n\t\t\t\t\t</span>\n\t\t\t\t\t<a href data-action=\"btn_clear_values\">\n\t\t\t\t\t\t${__('Clear values')}\n\t\t\t\t\t</a>\n\t\t\t</div>`;\n\t\t/* eslint-disable indent */\n\n\t\treturn `\n\t\t\t${item_found_status}\n\t\t\t${item_add_to_cart}\n\t\t`;\n\t}\n\n\tbtn_add_to_cart(e) {\n\t\tif (frappe.session.user !== 'Guest') {\n\t\t\tlocalStorage.removeItem(this.get_cache_key());\n\t\t}\n\t\tconst item_code = $(e.currentTarget).data('item-code');\n\t\tconst additional_notes = Object.keys(this.range_values || {}).map(attribute => {\n\t\t\treturn `${attribute}: ${this.range_values[attribute]}`;\n\t\t}).join('\\n');\n\t\twebshop.webshop.shopping_cart.update_cart({\n\t\t\titem_code,\n\t\t\tadditional_notes,\n\t\t\tqty: 1\n\t\t});\n\t\tthis.dialog.hide();\n\t}\n\n\tbtn_clear_values() {\n\t\tthis.dialog.fields_list.forEach(f => {\n\t\t\tif (f.df?.options) {\n\t\t\t\tf.df.options = f.df.options.map(option => {\n\t\t\t\t\toption.disabled = false;\n\t\t\t\t\treturn option;\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\tthis.dialog.clear();\n\t\tthis.on_attribute_selection();\n\t}\n\n\tappend_status_area() {\n\t\tthis.dialog.$status_area = $('<div class=\"status-area mt-5\">');\n\t\tthis.dialog.$wrapper.find('.modal-body').append(this.dialog.$status_area);\n\t\tthis.dialog.$wrapper.on('click', '[data-action]', (e) => {\n\t\t\te.preventDefault();\n\t\t\tconst $target = $(e.currentTarget);\n\t\t\tconst action = $target.data('action');\n\t\t\tconst method = this[action];\n\t\t\tmethod.call(this, e);\n\t\t});\n\t\tthis.dialog.$wrapper.addClass('item-configurator-dialog');\n\t}\n\n\tget_next_attribute_and_values(selected_attributes) {\n\t\treturn this.call('webshop.webshop.variant_selector.utils.get_next_attribute_and_values', {\n\t\t\titem_code: this.item_code,\n\t\t\tselected_attributes\n\t\t});\n\t}\n\n\tget_attributes_and_values() {\n\t\treturn this.call('webshop.webshop.variant_selector.utils.get_attributes_and_values', {\n\t\t\titem_code: this.item_code\n\t\t});\n\t}\n\n\tget_cache_key() {\n\t\treturn `configure:${this.item_code}`;\n\t}\n\n\tcall(method, args) {\n\t\t// promisified frappe.call\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tfrappe.call(method, args)\n\t\t\t\t.then(r => resolve(r.message))\n\t\t\t\t.fail(reject);\n\t\t});\n\t}\n}\n\nfunction set_continue_configuration() {\n\tconst $btn_configure = $('.btn-configure');\n\tconst { itemCode } = $btn_configure.data();\n\n\tif (localStorage.getItem(`configure:${itemCode}`)) {\n\t\t$btn_configure.text(__('Continue Selection'));\n\t} else {\n\t\t$btn_configure.text(__('Select Variant'));\n\t}\n}\n\nfrappe.ready(() => {\n\tconst $btn_configure = $('.btn-configure');\n\tif (!$btn_configure.length) return;\n\tconst { itemCode, itemName } = $btn_configure.data();\n\n\tset_continue_configuration();\n\n\t$btn_configure.on('click', () => {\n\t\t$btn_configure.prop('disabled', true);\n\t\tnew ItemConfigure(itemCode, itemName);\n\t});\n});\n"
  },
  {
    "path": "webshop/templates/generators/item/item_details.html",
    "content": "{% set width_class = \"expand\" if not slides else \"\" %}\n{% set cart_settings = shopping_cart.cart_settings %}\n{% set product_info = shopping_cart.product_info %}\n{% set price_info = product_info.get('price') or {} %}\n\n<div class=\"col-md-7 product-details {{ width_class }}\">\n\t<div class=\"d-flex\">\n\t\t<!-- title -->\n\t\t<div class=\"product-title col-11\" itemprop=\"name\">\n\t\t\t{{ _(doc.web_item_name) }}\n\t\t</div>\n\n\t\t<!-- Wishlist -->\n\t\t{% if cart_settings.enable_wishlist %}\n\t\t\t<div class=\"like-action-item-fp like-action {{ 'like-action-wished' if wished else ''}} ml-2\"\n\t\t\t\tdata-item-code=\"{{ doc.item_code }}\">\n\t\t\t\t<svg class=\"icon sm\">\n\t\t\t\t\t<use class=\"{{ 'wished' if wished else 'not-wished' }} wish-icon\" href=\"#icon-heart\"></use>\n\t\t\t\t</svg>\n\t\t\t</div>\n\t\t{% endif %}\n\t</div>\n\n\t<div itemprop=\"aggregateRating\" itemscope itemtype=\"https://schema.org/AggregateRating\">\n\t</div>\n\n\t<div itemprop=\"review\" itemscope itemtype=\"https://schema.org/Review\">\n\t</div>\n\n\t<p class=\"product-code\">\n\t\t<span class=\"product-item-group\">\n\t\t\t{{ _(doc.item_group) }}\n\t\t</span>\n\t\t<span class=\"product-item-code\" itemprop=\"name\">\n\t\t\t{{ _(\"Item Code\") }}:\n\t\t</span>\n\t\t<span itemprop=\"name\">{{ _(doc.item_code) }}</span>\n\t</p>\n\t{% if has_variants %}\n\t\t<!-- configure template -->\n\t\t{% include \"templates/generators/item/item_configure.html\" %}\n\t{% else %}\n\t\t<!-- add variant to cart -->\n\t\t{% include \"templates/generators/item/item_add_to_cart.html\" %}\n\t{% endif %}\n\t<!-- description -->\n\t<div class=\"product-description\" itemprop=\"description\">\n\t{% if frappe.utils.strip_html(doc.web_long_description or '') %}\n\t\t{{ _(doc.web_long_description) | safe }}\n\t{% elif frappe.utils.strip_html(doc.description or '')  %}\n\t\t{{ _(doc.description) | safe }}\n\t{% else %}\n\t\t{{ \"\" }}\n\t{% endif  %}\n\t</div>\n</div>\n\n{% block base_scripts %}\n<!-- js should be loaded in body! -->\n<script type=\"text/javascript\" src=\"/assets/frappe/js/lib/jquery/jquery.min.js\"></script>\n{% endblock %}\n\n<script>\n\t$('.page_content').on('click', '.like-action-item-fp', (e) => {\n\t\t\t// Bind action on wishlist button\n\t\t\tconst $btn = $(e.currentTarget);\n\t\t\twebshop.webshop.wishlist.wishlist_action($btn);\n\t\t});\n</script>\n"
  },
  {
    "path": "webshop/templates/generators/item/item_image.html",
    "content": "{% set column_size = 5 if slides else 4 %}\n<div class=\"col-md-{{ column_size }} h-100 d-flex mb-4\">\n\t{% if slides %}\n\t\t<div class=\"item-slideshow d-flex flex-column mr-3\">\n\t\t\t{% for item in slides %}\n\t\t\t<img class=\"item-slideshow-image mb-2 {% if loop.first %}active{% endif %}\"\n\t\t\t\t\tsrc=\"{{ item.image }}\" alt=\"{{ item.heading }}\">\n\t\t\t{% endfor %}\n\t\t</div>\n\t\t{{ product_image(slides[0].image, 'product-image') }}\n\t\t<!-- Simple image slideshow -->\n\t\t<script>\n\t\t\tfrappe.ready(() => {\n\t\t\t\t$('.page_content').on('click', '.item-slideshow-image', (e) => {\n\t\t\t\t\tconst $img = $(e.currentTarget);\n\t\t\t\t\tconst link = $img.prop('src');\n\t\t\t\t\tconst $product_image = $('.product-image');\n\t\t\t\t\t$product_image.find('a').prop('href', link);\n\t\t\t\t\t$product_image.find('img').prop('src', link);\n\n\t\t\t\t\t$('.item-slideshow-image').removeClass('active');\n\t\t\t\t\t$img.addClass('active');\n\t\t\t\t});\n\t\t\t})\n\t\t</script>\n\t{% else %}\n\t\t{{ product_image(doc.website_image, alt=doc.website_image_alt or doc.item_name) }}\n\t{% endif %}\n\n\t<!-- Simple image preview -->\n\n\t<div class=\"image-zoom-view\" style=\"display: none;\">\n\t\t<button type=\"button\" class=\"close\" aria-label=\"Close\">\n\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n\t\t\tstroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-x\">\n\t\t\t\t<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n\t\t\t\t<line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n\t\t\t</svg>\n\t\t</button>\n\t</div>\n</div>\n<style>\n\t.website-image {\n\t\tcursor: pointer;\n\t}\n\n\t.image-zoom-view {\n\t\tposition: fixed;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\theight: 100vh;\n\t\twidth: 100vw;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tbackground: rgba(0, 0, 0, 0.8);\n\t\tz-index: 1080;\n\t}\n\n\t.image-zoom-view img {\n\t\tmax-height: 100%;\n\t\tmax-width: 100%;\n\t}\n\n\t.image-zoom-view button {\n\t\tposition: absolute;\n\t\tright: 3rem;\n\t\ttop: 2rem;\n\t}\n\n\t.image-zoom-view svg {\n\t\tcolor: var(--white);\n\t}\n</style>\n<script>\n\tfrappe.ready(() => {\n\t\tconst $zoom_wrapper = $('.image-zoom-view');\n\n\t\t$('.website-image').on('click', (e) => {\n\t\t\te.preventDefault();\n\t\t\tconst $img = $(e.target);\n\t\t\tconst src = $img.prop('src');\n\t\t\tif (!src) return;\n\t\t\tshow_preview(src);\n\t\t});\n\n\t\t$zoom_wrapper.on('click', 'button', hide_preview);\n\n\t\t$(document).on('keydown', (e) => {\n\t\t\tif (e.key === 'Escape') {\n\t\t\t\thide_preview();\n\t\t\t}\n\t\t});\n\n\t\tfunction show_preview(src) {\n\t\t\t$zoom_wrapper.show();\n\t\t\tconst $img = $(`<img src=\"${src}\">`)\n\t\t\t$zoom_wrapper.append($img);\n\t\t}\n\n\t\tfunction hide_preview() {\n\t\t\t$zoom_wrapper.find('img').remove();\n\t\t\t$zoom_wrapper.hide();\n\t\t}\n\t})\n</script>\n"
  },
  {
    "path": "webshop/templates/generators/item/item_inquiry.html",
    "content": "{% if shopping_cart and shopping_cart.cart_settings.enabled %}\n{% set cart_settings = shopping_cart.cart_settings %}\n\t{% if cart_settings.show_contact_us_button | int %}\n\t\t<button class=\"btn btn-inquiry font-md w-30-40\" data-item-code=\"{{ doc.name }}\">\n\t\t\t{{ _('Contact Us') }}\n\t\t</button>\n\t{% endif %}\n<script>\n{% include \"templates/generators/item/item_inquiry.js\" %}\n</script>\n{% endif %}\n"
  },
  {
    "path": "webshop/templates/generators/item/item_inquiry.js",
    "content": "frappe.ready(() => {\n\tconst d = new frappe.ui.Dialog({\n\t\ttitle: __('Contact Us'),\n\t\tfields: [\n\t\t\t{\n\t\t\t\tfieldtype: 'Data',\n\t\t\t\tlabel: __('Full Name'),\n\t\t\t\tfieldname: 'lead_name',\n\t\t\t\treqd: 1\n\t\t\t},\n\t\t\t{\n\t\t\t\tfieldtype: 'Data',\n\t\t\t\tlabel: __('Organization Name'),\n\t\t\t\tfieldname: 'company_name',\n\t\t\t},\n\t\t\t{\n\t\t\t\tfieldtype: 'Data',\n\t\t\t\tlabel: __('Email'),\n\t\t\t\tfieldname: 'email_id',\n\t\t\t\toptions: 'Email',\n\t\t\t\treqd: 1\n\t\t\t},\n\t\t\t{\n\t\t\t\tfieldtype: 'Data',\n\t\t\t\tlabel: __('Phone Number'),\n\t\t\t\tfieldname: 'phone',\n\t\t\t\toptions: 'Phone',\n\t\t\t\treqd: 1\n\t\t\t},\n\t\t\t{\n\t\t\t\tfieldtype: 'Data',\n\t\t\t\tlabel: __('Subject'),\n\t\t\t\tfieldname: 'subject',\n\t\t\t\treqd: 1\n\t\t\t},\n\t\t\t{\n\t\t\t\tfieldtype: 'Text',\n\t\t\t\tlabel: __('Message'),\n\t\t\t\tfieldname: 'message',\n\t\t\t\treqd: 1\n\t\t\t}\n\t\t],\n\t\tprimary_action: send_inquiry,\n\t\tprimary_action_label: __('Send')\n\t});\n\n\tfunction send_inquiry() {\n\t\tconst values = d.get_values();\n\t\tconst doc = Object.assign({}, values);\n\t\tdelete doc.subject;\n\t\tdelete doc.message;\n\n\t\td.hide();\n\n\t\tfrappe.call('webshop.webshop.shopping_cart.cart.create_lead_for_item_inquiry', {\n\t\t\tlead: doc,\n\t\t\tsubject: values.subject,\n\t\t\tmessage: values.message\n\t\t}).then(r => {\n\t\t\tif (r.message) {\n\t\t\t\td.clear();\n\t\t\t}\n\t\t});\n\t}\n\n\t$('.btn-inquiry').click((e) => {\n\t\tconst $btn = $(e.target);\n\t\tconst item_code = $btn.data('item-code');\n\t\td.set_value('subject', 'Inquiry about ' + item_code);\n\t\tif (!['Administrator', 'Guest'].includes(frappe.session.user)) {\n\t\t\td.set_value('email_id', frappe.session.user);\n\t\t\td.set_value('lead_name', frappe.get_cookie('full_name'));\n\t\t}\n\n\t\td.show();\n\t});\n});\n"
  },
  {
    "path": "webshop/templates/generators/item/item_reviews.html",
    "content": "{% from \"webshop/templates/includes/macros.html\" import user_review, ratings_summary %}\n\n<div class=\"mt-4 ratings-reviews-section\">\n\t\t<!-- Title and Action -->\n\t\t<div class=\"w-100 mt-4 mb-2 d-flex\">\n\t\t\t<div class=\"reviews-header col-9\">\n\t\t\t\t{{ _(\"Customer Reviews\") }}\n\t\t\t</div>\n\n\t\t\t<div class=\"write-a-review-btn col-3\">\n\t\t\t\t<!-- Write a Review for legitimate users -->\n\t\t\t\t{% if frappe.session.user != \"Guest\" and user_is_customer %}\n\t\t\t\t\t<button class=\"btn btn-write-review\"\n\t\t\t\t\t\tdata-web-item=\"{{ doc.name }}\">\n\t\t\t\t\t\t{{ _(\"Write a Review\") }}\n\t\t\t\t\t</button>\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Summary -->\n\t\t{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}\n\n\n\t<!-- Reviews and Comments -->\n\t<div class=\"mt-8\">\n\t\t{% if reviews %}\n\t\t\t{{ user_review(reviews) }}\n\n\t\t\t{% if total_reviews > 4 %}\n\t\t\t\t<div class=\"mt-6 mb-6\"style=\"color: var(--primary);\">\n\t\t\t\t\t<a href=\"/customer_reviews?web_item={{ doc.name }}\">{{ _(\"View all reviews\") }}</a>\n\t\t\t\t</div>\n\t\t\t{% endif %}\n\n\t\t{% else %}\n\t\t\t<h6 class=\"text-muted mt-6\">\n\t\t\t\t{{ _(\"No Reviews\") }}\n\t\t\t</h6>\n\t\t{% endif %}\n\t</div>\n</div>\n\n<script>\n\tfrappe.ready(() => {\n\t\t$('.page_content').on('click', '.btn-write-review', (e) => {\n\t\t\t// Bind action on write a review button\n\t\t\tconst $btn = $(e.currentTarget);\n\n\t\t\tlet d = new frappe.ui.Dialog({\n\t\t\t\ttitle: __(\"Write a Review\"),\n\t\t\t\tfields: [\n\t\t\t\t\t{fieldname: \"title\", fieldtype: \"Data\", label: __(\"Headline\"), reqd: 1},\n\t\t\t\t\t{fieldname: \"rating\", fieldtype: \"Rating\", label: __(\"Overall Rating\"), reqd: 1},\n\t\t\t\t\t{fieldtype: \"Section Break\"},\n\t\t\t\t\t{fieldname: \"comment\", fieldtype: \"Small Text\", label: __(\"Your Review\")}\n\t\t\t\t],\n\t\t\t\tprimary_action: function() {\n\t\t\t\t\tvar data = d.get_values();\n\t\t\t\t\tfrappe.call({\n\t\t\t\t\t\tmethod: \"webshop.webshop.doctype.item_review.item_review.add_item_review\",\n\t\t\t\t\t\targs: {\n\t\t\t\t\t\t\tweb_item: \"{{ doc.name }}\",\n\t\t\t\t\t\t\ttitle: data.title,\n\t\t\t\t\t\t\trating: data.rating,\n\t\t\t\t\t\t\tcomment: data.comment\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfreeze: true,\n\t\t\t\t\t\tfreeze_message: __(\"Submitting Review ...\"),\n\t\t\t\t\t\tcallback: function(r) {\n\t\t\t\t\t\t\tif(!r.exc) {\n\t\t\t\t\t\t\t\tfrappe.msgprint({\n\t\t\t\t\t\t\t\t\tmessage: __(\"Thank you for the review\"),\n\t\t\t\t\t\t\t\t\ttitle: __(\"Review Submitted\"),\n\t\t\t\t\t\t\t\t\tindicator: \"green\"\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\td.hide();\n\t\t\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tprimary_action_label: __(\"Submit\")\n\t\t\t});\n\t\t\td.show();\n\t\t});\n\t});\n</script>\n"
  },
  {
    "path": "webshop/templates/generators/item/item_specifications.html",
    "content": "<!-- Is reused to render within tabs as well as independently -->\n{% if website_specifications %}\n<div class=\"{{ 'mt-2' if not show_tabs else 'mt-5'}} item-website-specification\">\n\t<div class=\"col-md-11\">\n\t\t{% if not show_tabs %}\n\t\t\t<div class=\"product-title mb-5 mt-4\">\n\t\t\t\t{{ _(\"Product Details\") }}\n\t\t\t</div>\n\t\t{% endif %}\n\t\t<table class=\"table\">\n\t\t{% for d in website_specifications -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"spec-label\">{{ _(d.label) }}</td>\n\t\t\t\t<td class=\"spec-content\">{{ _(d.description) }}</td>\n\t\t\t</tr>\n\t\t{%- endfor %}\n\t\t</table>\n\t</div>\n</div>\n{% endif %}\n"
  },
  {
    "path": "webshop/templates/generators/item_group.html",
    "content": "{% from \"webshop/templates/includes/macros.html\" import field_filter_section, attribute_filter_section, discount_range_filters %}\n{% extends \"templates/web.html\" %}\n\n{% block header %}\n<div class=\"mb-6\">{{ _(item_group_name) }}</div>\n{% endblock header %}\n\n{% block script %}\n<script type=\"text/javascript\" src=\"/all-products/index.js\"></script>\n{% endblock %}\n\n{% block breadcrumbs %}\n<div class=\"item-breadcrumbs small text-muted\">\n\t{% include \"templates/includes/breadcrumbs.html\" %}\n</div>\n{% endblock %}\n\n{% block page_content %}\n<div class=\"item-group-content\" itemscope itemtype=\"http://schema.org/Product\"\n\tdata-item-group=\"{{ name }}\">\n\t<div class=\"item-group-slideshow\">\n\t\t{% if slideshow %} <!-- slideshow -->\n\t\t\t{{ web_block(\n\t\t\t\t\"Hero Slider\",\n\t\t\t\tvalues=slideshow,\n\t\t\t\tadd_container=0,\n\t\t\t\tadd_top_padding=0,\n\t\t\t\tadd_bottom_padding=0,\n\t\t\t) }}\n\t\t{% endif %}\n\n\t\t{% if description %} <!-- description -->\n\t\t<div class=\"item-group-description text-muted mb-5\" itemprop=\"description\">{{ description or \"\"}}</div>\n\t\t{% endif %}\n\t</div>\n\t<div class=\"row\">\n\t\t<div id=\"product-listing\" class=\"col-12 order-2 col-md-9 order-md-2 item-card-group-section\">\n\t\t\t<!-- Products Rendered in all-products/index.js-->\n\t\t</div>\n\n\t\t<div class=\"col-12 order-1 col-md-3 order-md-1\">\n\t\t\t<div class=\"collapse d-md-block mr-4 filters-section\" id=\"product-filters\">\n\t\t\t\t<div class=\"d-flex justify-content-between align-items-center mb-5 title-section\">\n\t\t\t\t\t<div class=\"mb-4 filters-title\" > {{ _('Filters') }} </div>\n\t\t\t\t\t<a class=\"mb-4 clear-filters\" href=\"/{{ doc.route }}\">{{ _('Clear All') }}</a>\n\t\t\t\t</div>\n\t\t\t\t<!-- field filters -->\n\t\t\t\t{{ field_filter_section(field_filters) }}\n\n\t\t\t\t<!-- attribute filters -->\n\t\t\t\t{{ attribute_filter_section(attribute_filters) }}\n\n\t\t\t</div>\n\n\t\t</div>\n\t</div>\n</div>\n\n<script>\n\tfrappe.ready(() => {\n\t\t$('.btn-prev, .btn-next').click((e) => {\n\t\t\tconst $btn = $(e.target);\n\t\t\t$btn.prop('disabled', true);\n\t\t\tconst start = $btn.data('start');\n\t\t\tlet query_params = frappe.utils.get_query_params();\n\t\t\tquery_params.start = start;\n\t\t\tlet path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);\n\t\t\twindow.location.href = path;\n\t\t});\n\t});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/includes/cart/address_card.html",
    "content": "<div class=\"card address-card h-100\">\n\t<div class=\"btn btn-sm btn-default btn-change-address font-md\" style=\"position: absolute; right: 0; top: 0;\">\n\t\t{{ _('Change') }}\n\t</div>\n\t<div class=\"card-body p-0\">\n\t\t<div class=\"card-title\">{{ address.title }}</div>\n\t\t<div class=\"card-text mb-2\">\n\t\t\t{{ address.display }}\n\t\t</div>\n\t\t<a href=\"/address/{{address.name}}\" class=\"card-link\">\n\t\t\t<svg class=\"icon icon-sm\">\n\t\t\t\t<use href=\"#icon-edit\"></use>\n\t\t\t</svg>\n\t\t\t{{ _('Edit') }}\n\t\t</a>\n\t</div>\n</div>\n"
  },
  {
    "path": "webshop/templates/includes/cart/address_picker_card.html",
    "content": "<div class=\"card address-card h-100\">\n\t<div class=\"check\" style=\"position: absolute; right: 15px; top: 15px;\">\n\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-check\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg>\n\t</div>\n\t<div class=\"card-body\">\n\t\t<h5 class=\"card-title\">{{ address.title }}</h5>\n\t\t<p class=\"card-text text-muted\">\n\t\t\t{{ address.display }}\n\t\t</p>\n\t\t<a href=\"/address/{{address.name}}\" class=\"card-link\">{{ _('Edit') }}</a>\n\t</div>\n</div>\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_address.html",
    "content": "{% from \"webshop/templates/includes/cart/cart_macros.html\" import show_address %}\n\n{% if addresses | length == 1%}\n\t{% set select_address = True %}\n{% endif %}\n\n<div class=\"mb-3 frappe-card p-5\" data-section=\"shipping-address\">\n\t<div class=\"d-flex\">\n\t\t<div class=\"col-6 address-header\"><h6>{{ _(\"Shipping Address\") }}</h6></div>\n\t\t<div class=\"col-6\" style=\"padding: 0;\">\n\t\t\t<a class=\"ml-4 btn-new-address\" role=\"button\">{{ _(\"Add a new address\") }}</a>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t{% for address in shipping_addresses %}\n\t{% if doc.shipping_address_name == address.name %}\n\t<div class=\"row no-gutters\" data-fieldname=\"shipping_address_name\">\n\t\t<div class=\"w-100 address-container\" data-address-name=\"{{address.name}}\" data-address-type=\"shipping\" data-active>\n\t\t\t{% include \"templates/includes/cart/address_card.html\" %}\n\t\t</div>\n\t</div>\n\t{% endif %}\n\t{% endfor %}\n</div>\n\n<!-- Billing Address -->\n<div class=\"checkbox ml-1 mb-2\">\n\t<label for=\"input_same_billing\">\n\t\t<input type=\"checkbox\" class=\"product-filter\" id=\"input_same_billing\" checked style=\"width: 14px !important\">\n\t\t<span class=\"label-area font-md\">{{ _('Billing Address is same as Shipping Address') }}</span>\n\t</label>\n</div>\n\n\n<div class=\"mb-3 frappe-card p-5\" data-section=\"billing-address\">\n\t<div class=\"d-flex\">\n\t\t<div class=\"col-6 address-header\"><h6>{{ _(\"Billing Address\") }}</h6></div>\n\t\t<div class=\"col-6\" style=\"padding: 0;\">\n\t\t\t<a class=\"ml-4 btn-new-address\" role=\"button\">{{ _(\"Add a new address\") }}</a>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t{% for address in billing_addresses %}\n\t\t{% if doc.customer_address == address.name %}\n\t\t<div class=\"row no-gutters\" data-fieldname=\"customer_address\">\n\t\t\t<div class=\"w-100 address-container\" data-address-name=\"{{address.name}}\" data-address-type=\"billing\" data-active>\n\t\t\t\t\t{% include \"templates/includes/cart/address_card.html\" %}\n\t\t\t\t</div>\n\t\t</div>\n\t\t{% endif %}\n\t{% endfor %}\n</div>\n\n\n<script>\nfrappe.ready(() => {\n\t$(document).on('click', '.address-card', (e) => {\n\t\tconst $target = $(e.currentTarget);\n\t\tconst $section = $target.closest('[data-section]');\n\t\t$section.find('.address-card').removeClass('active');\n\t\t$target.addClass('active');\n\t});\n\n\t$('#input_same_billing').change((e) => {\n\t\tconst $check = $(e.target);\n\t\ttoggle_billing_address_section(!$check.is(':checked'));\n\t});\n\n\t$('.btn-new-address').click(() => {\n\t\tconst d = new frappe.ui.Dialog({\n\t\t\ttitle: '{{_(\"New Address\") }}',\n\t\t\tfields: [\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"Address Title\") }}',\n\t\t\t\t\tfieldname: 'address_title',\n\t\t\t\t\tfieldtype: 'Data',\n\t\t\t\t\treqd: 1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"Address Line 1\") }}',\n\t\t\t\t\tfieldname: 'address_line1',\n\t\t\t\t\tfieldtype: 'Data',\n\t\t\t\t\treqd: 1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"Address Line 2\") }}',\n\t\t\t\t\tfieldname: 'address_line2',\n\t\t\t\t\tfieldtype: 'Data'\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"City/Town\") }}',\n\t\t\t\t\tfieldname: 'city',\n\t\t\t\t\tfieldtype: 'Data',\n\t\t\t\t\treqd: 1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"State\") }}',\n\t\t\t\t\tfieldname: 'state',\n\t\t\t\t\tfieldtype: 'Data'\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"Country\") }}',\n\t\t\t\t\tfieldname: 'country',\n\t\t\t\t\tfieldtype: 'Link',\n\t\t\t\t\toptions: 'Country',\n\t\t\t\t\tonly_select: true,\n\t\t\t\t\treqd: 1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfieldname: \"column_break0\",\n\t\t\t\t\tfieldtype: \"Column Break\",\n\t\t\t\t\twidth: \"50%\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"Address Type\") }}',\n\t\t\t\t\tfieldname: 'address_type',\n\t\t\t\t\tfieldtype: 'Select',\n\t\t\t\t\toptions: [\n\t\t\t\t\t\t'Billing',\n\t\t\t\t\t\t'Shipping'\n\t\t\t\t\t],\n\t\t\t\t\treqd: 1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: '{{ _(\"Postal Code\") }}',\n\t\t\t\t\tfieldname: 'pincode',\n\t\t\t\t\tfieldtype: 'Data'\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfieldname: \"phone\",\n\t\t\t\t\tfieldtype: \"Data\",\n\t\t\t\t\tlabel: '{{ _(\"Phone\") }}',\n\t\t\t\t\treqd: 1\n\t\t\t\t},\n\t\t\t],\n\t\t\tprimary_action_label: '{{ _(\"Save\") }}',\n\t\t\tprimary_action: (values) => {\n\t\t\t\tfrappe.call('webshop.webshop.shopping_cart.cart.add_new_address', { doc: values })\n\t\t\t\t\t.then(r => {\n\t\t\t\t\t\tfrappe.call({\n\t\t\t\t\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.update_cart_address\",\n\t\t\t\t\t\t\targs: {\n\t\t\t\t\t\t\t\taddress_type: r.message.address_type,\n\t\t\t\t\t\t\t\taddress_name: r.message.name\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcallback: function (r) {\n\t\t\t\t\t\t\t\td.hide();\n\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\n\t\t\t}\n\t\t})\n\n\t\td.show();\n\t});\n\n\tfunction setup_state() {\n\t\tconst shipping_address = $('[data-section=\"shipping-address\"]')\n\t\t\t.find('[data-address-name][data-active]').attr('data-address-name');\n\n\t\tconst billing_address = $('[data-section=\"billing-address\"]')\n\t\t\t.find('[data-address-name][data-active]').attr('data-address-name');\n\n\t\t$('#input_same_billing').prop('checked', shipping_address === billing_address).trigger('change');\n\n\t\tif (!shipping_address && !billing_address) {\n\t\t\t$('#input_same_billing').prop('checked', true).trigger('change');\n\t\t}\n\n\t\tif (shipping_address) {\n\t\t\t$(`[data-section=\"shipping-address\"] [data-address-name=\"${shipping_address}\"] .address-card`).addClass('active');\n\t\t}\n\t\tif (billing_address) {\n\t\t\t$(`[data-section=\"billing-address\"] [data-address-name=\"${billing_address}\"] .address-card`).addClass('active');\n\t\t}\n\t}\n\n\tsetup_state();\n\n\tfunction toggle_billing_address_section(flag) {\n\t\t$('[data-section=\"billing-address\"]').toggle(flag);\n\t}\n});\n</script>\n\n<script>\n\tfrappe.ready(() => {\n\t\tfunction get_update_address_dialog() {\n\t\t\tlet d = new frappe.ui.Dialog({\n\t\t\t\ttitle: \"Select Address\",\n\t\t\t\tfields: [{\n\t\t\t\t\t'fieldtype': 'HTML',\n\t\t\t\t\t'fieldname': 'address_picker',\n\t\t\t\t}],\n\t\t\t\tprimary_action_label: __('Set Address'),\n\t\t\t\tprimary_action: () => {\n\t\t\t\t\tconst $card = d.$wrapper.find('.address-card.active');\n\t\t\t\t\tconst address_type = $card.closest('[data-address-type]').attr('data-address-type');\n\t\t\t\t\tconst address_name = $card.closest('[data-address-name]').attr('data-address-name');\n\t\t\t\t\tfrappe.call({\n\t\t\t\t\t\ttype: \"POST\",\n\t\t\t\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.update_cart_address\",\n\t\t\t\t\t\tfreeze: true,\n\t\t\t\t\t\targs: {\n\t\t\t\t\t\t\taddress_type,\n\t\t\t\t\t\t\taddress_name\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcallback: function(r) {\n\t\t\t\t\t\t\td.hide();\n\t\t\t\t\t\t\tif (!r.exc) {\n\t\t\t\t\t\t\t\t$(\".cart-tax-items\").html(r.message.total);\n\t\t\t\t\t\t\t\tshopping_cart.parent.find(\n\t\t\t\t\t\t\t\t\t`.address-container[data-address-type=\"${address_type}\"]`\n\t\t\t\t\t\t\t\t).html(r.message.address);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn d;\n\t\t}\n\n\t\tfunction get_address_template(type) {\n\t\t\treturn {\n\t\t\t\tshipping: `<div class=\"mb-3\" data-section=\"shipping-address\">\n\t\t\t\t\t<div class=\"row no-gutters\" data-fieldname=\"shipping_address_name\">\n\t\t\t\t\t\t{% for address in shipping_addresses %}\n\t\t\t\t\t\t\t<div class=\"mr-3 mb-3 w-100\" data-address-name=\"{{address.name}}\" data-address-type=\"shipping\"\n\t\t\t\t\t\t\t\t{% if doc.shipping_address_name == address.name %} data-active {% endif %}>\n\t\t\t\t\t\t\t\t{% include \"templates/includes/cart/address_picker_card.html\" %}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t</div>\n\t\t\t\t</div>`,\n\t\t\t\tbilling: `<div class=\"mb-3\" data-section=\"billing-address\">\n\t\t\t\t\t<div class=\"row no-gutters\" data-fieldname=\"customer_address\">\n\t\t\t\t\t\t{% for address in billing_addresses %}\n\t\t\t\t\t\t\t<div class=\"mr-3 mb-3 w-100\" data-address-name=\"{{address.name}}\" data-address-type=\"billing\"\n\t\t\t\t\t\t\t\t{% if doc.shipping_address_name == address.name %} data-active {% endif %}>\n\t\t\t\t\t\t\t\t{% include \"templates/includes/cart/address_picker_card.html\" %}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t</div>\n\t\t\t\t</div>`,\n\t\t\t}[type];\n\t\t}\n\n\t\t$(document).on('click', '.btn-change-address', (e) => {\n\t\t\tconst d = get_update_address_dialog();\n\t\t\tconst type = $(e.currentTarget).parents('.address-container').attr('data-address-type');\n\n\t\t\t$(d.get_field('address_picker').wrapper).html(\n\t\t\t\tget_address_template(type)\n\t\t\t);\n\n\t\t\td.show();\n\t\t});\n\t});\n</script>\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_address_picker.html",
    "content": "<div class=\"mb-3 frappe-card p-5\" data-section=\"shipping-address\">\n\t<h6>{{ _(\"Shipping Address\") }}</h6>\n</div>\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_dropdown.html",
    "content": "<div class=\"cart-dropdown-container\">\n\t<div id=\"cart-error\" class=\"alert alert-danger\"\n\t\tstyle=\"display: none;\"></div>\n\t<div class=\"row checkout-btn\">\n\t\t<div class=\"col-sm-12 col-xs-12\">\n\t\t\t<a href=\"/cart\" class=\"btn btn-block btn-primary\">{{ _(\"Checkout\") }}</a>\n\t\t</div>\n\t</div>\n\t<div class=\"row cart-items-dropdown cart-item-header text-muted\">\n\t\t<div class=\"col-sm-6 col-xs-6 h6 text-uppercase\">\n\t\t{{ _(\"Item\") }}\n\t\t</div>\n\t\t<div class=\"col-sm-6 col-xs-6 text-right h6 text-uppercase\">\n\t\t{{ _(\"Price\") }}\n\t\t</div>\n\t</div>\n\n\t{% if doc.items %}\n\t<div class=\"row cart-items-dropdown\">\n\t\t<div class=\"col-sm-12 col-xs-12\">\n\t\t\t{% include \"templates/includes/cart/cart_items_dropdown.html\" %}\n\t\t</div>\n\t</div>\n\t{% else %}\n\t<p>{{ _(\"Cart is Empty\") }}</p>\n\t{% endif %}\n</div>\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_items.html",
    "content": "{% from \"webshop/templates/includes/macros.html\" import product_image %}\n\n{% macro item_subtotal(item) %}\n\t<div>\n\t\t{{ item.get_formatted('amount') }}\n\t</div>\n\n\t{% if item.is_free_item %}\n\t\t<div class=\"text-success mt-4\">\n\t\t\t<span class=\"free-tag\">\n\t\t\t\t{{ _('FREE') }}\n\t\t\t</span>\n\t\t</div>\n\t{% else %}\n\t\t<span class=\"item-rate\">\n\t\t\t{{ _('Rate:') }} {{ item.get_formatted('rate') }}\n\t\t</span>\n\t{% endif %}\n{% endmacro %}\n\n{% for d in doc.items %}\n\t<tr data-name=\"{{ d.name }}\">\n\t\t<td style=\"width: 60%;\">\n\t\t\t<div class=\"d-flex\">\n\t\t\t\t<div class=\"cart-item-image mr-4\">\n\t\t\t\t\t{% if d.thumbnail %}\n\t\t\t\t\t\t{{ product_image(d.thumbnail, alt=\"d.web_item_name\", no_border=True) }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t\t<div class = \"no-image-cart-item\">\n\t\t\t\t\t\t\t{{ frappe.utils.get_abbr(d.web_item_name) or \"NA\" }}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"d-flex w-100\" style=\"flex-direction: column;\">\n\t\t\t\t\t<div class=\"item-title mb-1 mr-3\">\n\t\t\t\t\t\t{{ d.get(\"web_item_name\") or d.item_name }}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"item-subtitle mr-2\">\n\t\t\t\t\t\t{{ d.item_code }}\n\t\t\t\t\t</div>\n\t\t\t\t\t{%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}\n\t\t\t\t\t{% if variant_of %}\n\t\t\t\t\t<span class=\"item-subtitle mr-2\">\n\t\t\t\t\t\t{{ _('Variant of') }}\n\t\t\t\t\t\t<a href=\"{{frappe.db.get_value('Website Item', {'item_code': variant_of}, 'route') or '#'}}\">\n\t\t\t\t\t\t\t{{ variant_of }}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</span>\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t\t<div class=\"mt-2 notes\">\n\t\t\t\t\t\t\t<textarea data-item-code=\"{{d.item_code}}\" class=\"form-control\" rows=\"2\" placeholder=\"{{ _('Add notes') }}\">{{d.additional_notes or ''}}</textarea>\n\t\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</td>\n\n\t\t<!-- Qty column -->\n\t\t<td class=\"text-right\" style=\"width: 25%;\">\n\t\t\t<div class=\"d-flex\">\n\t\t\t\t{% set disabled = 'disabled' if d.is_free_item else '' %}\n\t\t\t\t<div class=\"input-group number-spinner mt-1 mb-4\">\n\t\t\t\t\t<span class=\"input-group-prepend d-sm-inline-block\">\n\t\t\t\t\t\t<button class=\"btn cart-btn\" data-dir=\"dwn\" {{ disabled }}>\n\t\t\t\t\t\t\t{{ '–' if not d.is_free_item else ''}}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</span>\n\n\t\t\t\t\t<input class=\"form-control text-center cart-qty\" value=\"{{ d.get_formatted('qty') }}\" data-item-code=\"{{ d.item_code }}\"\n\t\t\t\t\t\tstyle=\"max-width: 70px;\" {{ disabled }}>\n\n\t\t\t\t\t<span class=\"input-group-append d-sm-inline-block\">\n\t\t\t\t\t\t<button class=\"btn cart-btn\" data-dir=\"up\" {{ disabled }}>\n\t\t\t\t\t\t\t{{ '+' if not d.is_free_item else ''}}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\n\t\t\t\t<div>\n\t\t\t\t\t{% if not d.is_free_item %}\n\t\t\t\t\t\t<div class=\"remove-cart-item column-sm-view d-flex\" data-item-code=\"{{ d.item_code }}\">\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<svg class=\"icon sm remove-cart-item-logo\"\n\t\t\t\t\t\t\t\t\twidth=\"18\" height=\"18\" viewBox=\"0 0 18 18\"\n\t\t\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\" id=\"icon-close\">\n\t\t\t\t\t\t\t\t\t<path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M4.146 11.217a.5.5 0 1 0 .708.708l3.182-3.182 3.181 3.182a.5.5 0 1 0 .708-.708l-3.182-3.18 3.182-3.182a.5.5 0 1 0-.708-.708l-3.18 3.181-3.183-3.182a.5.5 0 0 0-.708.708l3.182 3.182-3.182 3.181z\" stroke-width=\"0\"></path>\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t</div>\n\t\t\t</div>\n\n\n\t\t\t<!-- Shown on mobile view, else hidden -->\n\t\t\t{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}\n\t\t\t\t<div class=\"text-right sm-item-subtotal\">\n\t\t\t\t\t{{ item_subtotal(d) }}\n\t\t\t\t</div>\n\t\t\t{% endif %}\n\t\t</td>\n\n\t\t<!-- Subtotal column -->\n\t\t{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}\n\t\t\t<td class=\"text-right item-subtotal column-sm-view w-100\">\n\t\t\t\t{{ item_subtotal(d) }}\n\t\t\t</td>\n\t\t{% endif %}\n\t</tr>\n{% endfor %}\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_items_dropdown.html",
    "content": "{% from \"webshop/templates/includes/order/order_macros.html\" import item_name_and_description_cart %}\n\n{% for d in doc.items %}\n<div class=\"row cart-dropdown\">\n    <div class=\"col-sm-8 col-xs-8 col-name-description\">\n        {{ item_name_and_description_cart(d) }}\n    </div>\n    <div class=\"col-sm-4 col-xs-4 text-right col-amount\">\n        {{ d.get_formatted(\"amount\") }}\n    </div>\n</div>\n{% endfor %}\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_items_total.html",
    "content": "<!-- Total at the end of the cart items -->\n<tr>\n\t<th></th>\n\t<th class=\"text-left item-grand-total\" colspan=\"1\">\n\t\t{{ _(\"Net Total\") }}\n\t</th>\n\t<th class=\"text-left item-grand-total totals\" colspan=\"3\">\n\t\t{{ doc.get_formatted(\"total\") }}\n\t</th>\n</tr>\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_macros.html",
    "content": "{% macro show_address(address, doc, fieldname, select_address=False) %}\n{% set selected=address.name==doc.get(fieldname) %}\n\n<div class=\"panel panel-default\">\n\t<div class=\"panel-heading\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-sm-10 address-title\"\n\t\t\t\tdata-address-name=\"{{ address.name }}\">\n                <strong>{{ address.name }}</strong></div>\n\t\t\t<div class=\"col-sm-2 text-right\">\n                <input type=\"checkbox\"\n                data-fieldname=\"{{ fieldname }}\"\n\t\t\t\tdata-address-name=\"{{ address.name}}\"\n                    {{ \"checked\" if selected else \"\" }}></div>\n\t\t</div>\n\t</div>\n\t<div class=\"panel-collapse\"\n        data-address-name=\"{{ address.name }}\">\n\t\t<div class=\"panel-body text-muted small\">{{ address.display }}</div>\n\t</div>\n</div>\n{% endmacro %}\n"
  },
  {
    "path": "webshop/templates/includes/cart/cart_payment_summary.html",
    "content": "<!-- Payment -->\n{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}\n<h6>\n\t{{ _(\"Payment Summary\") }}\n</h6>\n{% endif %}\n\n<div class=\"card h-100\">\n\t<div class=\"card-body p-0\">\n\t\t{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}\n\t\t\t<table class=\"table w-100\">\n\t\t\t\t<tr>\n\t\t\t\t\t{% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}\n\t\t\t\t\t<td class=\"bill-label\">{{ _(\"Net Total (\") + total_items + _(\" Items)\") }}</td>\n\t\t\t\t\t<td class=\"bill-content net-total text-right\">{{ doc.get_formatted(\"net_total\") }}</td>\n\t\t\t\t</tr>\n\n\t\t\t\t<!-- taxes -->\n\t\t\t\t{% for d in doc.taxes %}\n\t\t\t\t\t{% if d.tax_amount %}\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"bill-label\">\n\t\t\t\t\t\t\t\t{{ d.description }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"bill-content text-right\">\n\t\t\t\t\t\t\t\t{{ d.get_formatted(\"tax_amount\") }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t{% endif %}\n\t\t\t\t{% endfor %}\n\t\t\t</table>\n\n\t\t\t<!-- TODO: Apply Coupon Dialog-->\n\t\t\t<!-- {% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}\n\t\t\t{% if show_coupon_code %}\n\t\t\t\t<button class=\"btn btn-coupon-code w-100 text-left\">\n\t\t\t\t\t<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke=\"var(--gray-600)\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n\t\t\t\t\t\t<path d=\"M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t<path d=\"M15 9L9 15\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t<path d=\"M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z\" fill=\"white\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t\t<path d=\"M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z\" fill=\"white\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t\t\t</svg>\n\t\t\t\t\t<span class=\"ml-2\">Apply Coupon</span>\n\t\t\t\t</button>\n\t\t\t{% endif %} -->\n\n\t\t\t<table class=\"table w-100 grand-total mt-6\">\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"bill-content net-total\">{{ _(\"Grand Total\") }}</td>\n\t\t\t\t\t<td class=\"bill-content net-total text-right\">{{ doc.get_formatted(\"grand_total\") }}</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n\t\t{% endif %}\n\t</div>\n</div>\n\n<!-- TODO: Apply Coupon Dialog-->\n<!-- <script>\n\tfrappe.ready(() => {\n\t\t$('.btn-coupon-code').click((e) => {\n\t\t\tconst $btn = $(e.currentTarget);\n\t\t\tconst d = new frappe.ui.Dialog({\n\t\t\t\ttitle: __('Coupons'),\n\t\t\t\tfields: [\n\t\t\t\t\t{\n\t\t\t\t\t\tfieldname: 'coupons_area',\n\t\t\t\t\t\tfieldtype: 'HTML'\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t});\n\t\t\td.show();\n\t\t});\n\t});\n</script> -->"
  },
  {
    "path": "webshop/templates/includes/cart/coupon_code.html",
    "content": "{% if coupon_code %}\n<p class=\"h6\">Coupon code</p>\n<div\n    class=\"cart-coupon-code flex gap-2 align-items-center w-fit rounded mb-4\"\n    style=\"border: 2px dashed #ccc;\"\n>\n    <div class=\"px-2 py-1 small\"> {{ coupon_code}} </div>\n    <button class=\"btn bt-remove-coupon-code\">\n        <span>\n            <svg\n                class=\"icon sm remove-cart-item-logo\"\n                width=\"18\"\n                height=\"18\"\n                viewBox=\"0 0 18 18\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                id=\"icon-close\"\n            >\n                <path\n                    fill-rule=\"evenodd\"\n                    clip-rule=\"evenodd\"\n                    d=\"M4.146 11.217a.5.5 0 1 0 .708.708l3.182-3.182 3.181 3.182a.5.5 0 1 0 .708-.708l-3.182-3.18 3.182-3.182a.5.5 0 1 0-.708-.708l-3.18 3.181-3.183-3.182a.5.5 0 0 0-.708.708l3.182 3.182-3.182 3.181z\"\n                    stroke-width=\"0\"\n                ></path>\n            </svg>\n        </span>\n    </button>\n</div>\n{% endif %}\n"
  },
  {
    "path": "webshop/templates/includes/cart/place_order.html",
    "content": "<div class=\"card h-100\">\n\t<div class=\"card-body p-0\">\n\t\t{% if cart_settings.enable_checkout %}\n\t\t\t<button class=\"btn btn-primary btn-place-order font-md w-100\" type=\"button\">\n\t\t\t\t{{ _(\"Place Order\") }}\n\t\t\t</button>\n\t\t{% else %}\n\t\t\t<button class=\"btn btn-primary btn-request-for-quotation font-md w-100\" type=\"button\">\n\t\t\t\t{{ _(\"Request for Quote\") }}\n\t\t\t</button>\n\t\t{% endif %}\n\t</div>\n</div>"
  },
  {
    "path": "webshop/templates/includes/macros.html",
    "content": "{% macro product_image_square(website_image, css_class=\"\") %}\n<div class=\"product-image product-image-square h-100 rounded\n\t{% if not website_image -%} missing-image {%- endif %} {{ css_class }}\"\n\t{% if website_image -%}\n\tstyle=\"background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');\"\n\t{%- endif %}>\n</div>\n{% endmacro %}\n\n{% macro product_image(website_image, css_class=\"product-image\", alt=\"\", no_border=False) %}\n\t<div class=\"{{ 'border' if not no_border else ''}} text-center rounded {{ css_class }}\" style=\"overflow: hidden;\">\n\t\t{% if website_image %}\n\t\t\t<img itemprop=\"image\" class=\"website-image h-100 w-100\" alt=\"{{ _(alt) }}\" src=\"{{ frappe.utils.quoted(website_image) | abs_url }}\">\n\t\t{% else %}\n\t\t\t<div itemprop=\"image\" class=\"card-img-top no-image-item\">\n\t\t\t\t{{ frappe.utils.get_abbr(alt) or \"NA\" }}\n\t\t\t</div>\n\t\t{% endif %}\n\t</div>\n{% endmacro %}\n\n{% macro media_image(website_image, name, css_class=\"\") %}\n\t<div class=\"product-image sidebar-image-wrapper {{ css_class }}\">\n\t\t{% if not website_image -%}\n\t\t<div class=\"sidebar-standard-image\"> <div class=\"standard-image\" style=\"background-color: rgb(250, 251, 252);\">{{_(name)}}</div> </div>\n\t\t{%- endif %}\n\t\t{% if website_image -%}\n\t\t\t<a href=\"{{ frappe.utils.quoted(website_image) }}\">\n\t\t\t\t<img itemprop=\"image\" src=\"{{ frappe.utils.quoted(website_image) | abs_url }}\"\n\t\t\t\t\tclass=\"img-responsive img-thumbnail sidebar-image\" style=\"min-height:100%; min-width:100%;\">\n\t\t\t</a>\n\t\t{%- endif %}\n\t</div>\n{% endmacro %}\n\n{% macro render_homepage_section(section) %}\n\n{% if section.section_based_on == 'Custom HTML' and section.section_html %}\n\t{{ section.section_html }}\n{% elif section.section_based_on == 'Cards' %}\n<section class=\"container my-5\">\n\t<h3>{{ _(section.name) }}</h3>\n\n\t<div class=\"row\">\n\t\t{% for card in section.section_cards %}\n\t\t<div class=\"col-md-{{ section.column_value }} mb-4\">\n\t\t\t<div class=\"card h-100 justify-content-between\">\n\t\t\t\t{% if card.image %}\n\t\t\t\t<img itemprop=\"image\" class=\"card-img-top h-75\" src=\"{{ card.image }}\" loading=\"lazy\" alt=\"{{ _(card.title) }}\"></img>\n\t\t\t\t{% endif %}\n\t\t\t\t<div itemprop=\"image\" class=\"card-body\">\n\t\t\t\t\t<h5 class=\"card-title\">{{ _(card.title) }}</h5>\n\t\t\t\t\t<p class=\"card-subtitle mb-2 text-muted\">{{ _(card.subtitle) or '' }}</p>\n\t\t\t\t\t<p class=\"card-text\">{{ card.content or '' | truncate(140, True) }}</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"card-body flex-grow-0\">\n\t\t\t\t\t<a href=\"{{ card.route }}\" class=\"card-link\">{{ _('More details') }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t{% endfor %}\n\t</div>\n</section>\n{% endif %}\n\n{% endmacro %}\n\n{%- macro item_card(item, is_featured=False, is_full_width=False, align=\"Left\",template=\"\") -%}\n{%- set align_items_class = resolve_class({\n\t'align-items-end': align == 'Right',\n\t'align-items-center': align == 'Center',\n\t'align-items-start': align == 'Left',\n}) -%}\n{%- set col_size = 3 if is_full_width else 4 -%}\n{%- set title = item.web_item_name or item.item_name or item.item_code -%}\n{%- set title = title[:50] + \"...\" if title|len > 50 else title -%}\n{%- set image = item.website_image -%}\n{%- set description = item.website_description or item.description-%}\n\n{% if is_featured %}\n<div class=\"col-sm-{{ col_size*2 }} item-card\">\n\t<div class=\"card featured-item {{ align_items_class }}\" style=\"height: 360px;\">\n\t\t{% if image %}\n\t\t<div class=\"row no-gutters\">\n\t\t\t<div class=\"col-md-5 ml-4\">\n\t\t\t\t<img class=\"card-img\" src=\"{{ image }}\" alt=\"{{ _(title) }}\">\n\t\t\t</div>\n\t\t\t<div class=\"col-md-6\">\n\t\t\t\t{{ item_card_body(title, description, item, is_featured, align,template) }}\n\t\t\t</div>\n\t\t</div>\n\t\t{% else %}\n\t\t\t<div class=\"col-md-12\">\n\t\t\t\t{{ item_card_body(title, description, item, is_featured, align,template) }}\n\t\t\t</div>\n\t\t{% endif %}\n\t</div>\n</div>\n{% else %}\n<div class=\"col-sm-{{ col_size }} item-card\">\n\t<div class=\"card {{ align_items_class }}\" style=\"height: 360px;\">\n\t\t{% if image %}\n\t\t\t<div class=\"card-img-container\">\n\t\t\t\t<a href=\"/{{ item.route or '#' }}\" style=\"text-decoration: none;\">\n\t\t\t\t\t<img class=\"card-img\" src=\"{{ image }}\" alt=\"{{ _(title )}}\">\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t{% else %}\n\t\t<a href=\"/{{ item.route or '#' }}\" style=\"text-decoration: none;\">\n\t\t\t<div class=\"card-img-top no-image\">\n\t\t\t\t{{ frappe.utils.get_abbr(title) }}\n\t\t\t</div>\n\t\t</a>\n\t\t{% endif %}\n\t\t{{ item_card_body(title, description, item, is_featured, align,template) }}\n\t</div>\n</div>\n{% endif %}\n{%- endmacro -%}\n\n{%- macro item_card_body(title, description, item, is_featured, align,template) -%}\n{%- set align_class = resolve_class({\n\t'text-right': align == 'Right',\n\t'text-center': align == 'Center' and not is_featured,\n\t'text-left': align == 'Left' or is_featured,\n}) -%}\n<div class=\"card-body {{ align_class }}\" style=\"width:100%\">\n\t<div class=\"mt-4\">\n\t\t<a href=\"/{{ item.route or '#' }}\">\n\t\t\t<div class=\"product-title\">\n\t\t\t\t{{ title or '' }}\n\t\t\t</div>\n\t\t</a>\n\t</div>\n\t{% if is_featured %}\n\t\t<div class=\"product-description ellipsis text-muted\" style=\"white-space: normal;\">\n\t\t\t{{ _(description) or '' }}\n\t\t</div>\n\t{% else %}\n\t\t{% if template != \"Product Card\" %}\n\t\t<div class=\"product-category\">{{ item.item_group or '' }}</div>\n\t\t{% endif %}\n\t{% endif %}\n</div>\n{%- endmacro -%}\n\n\n{%- macro wishlist_card(item, settings) %}\n{%- set title = item.web_item_name or ''-%}\n{%- set title = title[:90] + \"...\" if title|len > 90 else title -%}\n<div class=\"col-sm-3 wishlist-card\">\n\t<div class=\"card text-center\">\n\t\t<div class=\"card-img-container\">\n\t\t\t<a href=\"/{{ item.route or '#' }}\" style=\"text-decoration: none;\">\n\t\t\t\t{% if item.image %}\n\t\t\t\t\t<img itemprop=\"image\" class=\"card-img\" src=\"{{ item.image }}\" alt=\"{{ _(title) }}\">\n\t\t\t\t{% else %}\n\t\t\t\t\t<div itemprop=\"image\" class=\"card-img-top no-image\">\n\t\t\t\t\t\t{{ frappe.utils.get_abbr(title) }}\n\t\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</a>\n\t\t\t<div class=\"remove-wish\" data-item-code=\"{{ item.item_code }}\">\n\t\t\t\t<svg class=\"icon icon-md remove-wish-icon\">\n\t\t\t\t\t<use class=\"close\" href=\"#icon-delete\"></use>\n\t\t\t\t</svg>\n\t\t\t</div>\n\t\t</div>\n\n\t\t{{ wishlist_card_body(item, title, settings) }}\n\t</div>\n</div>\n{%- endmacro -%}\n\n{%- macro wishlist_card_body(item, title, settings) %}\n<div class=\"card-body card-body-flex text-left\" style=\"width: 100%;\">\n\t<div class=\"mt-4\">\n\t\t<div class=\"product-title\">{{ _(title) or ''}}</div>\n\t\t<div class=\"product-category\">{{ _(item.item_group) or '' }}</div>\n\t</div>\n\t<div  class=\"product-price\" itemprop=\"offers\" itemscope itemtype=\"https://schema.org/AggregateOffer\">\n\t\t{{ item.get(\"formatted_price\") or '' }}\n\n\t\t{% if item.get(\"formatted_mrp\") %}\n\t\t\t<small class=\"ml-1 striked-price\">\n\t\t\t\t<s>{{ _(item.formatted_mrp) }}</s>\n\t\t\t</small>\n\t\t\t<small class=\"ml-1 product-info-green\" >\n\t\t\t\t{{ _(item.discount) + \" \" + _(\"OFF\")}}\n\t\t\t</small>\n\t\t{% endif %}\n\t</div>\n\n\t{% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %}\n\t\t<!-- Show move to cart button if in stock or if showing stock availability is disabled -->\n\t\t<button data-item-code=\"{{ item.item_code}}\"\n\t\t\tclass=\"btn btn-primary btn-add-to-cart-list btn-add-to-cart mt-2 w-100\">\n\t\t\t<span class=\"mr-2\">\n\t\t\t\t<svg class=\"icon icon-md\">\n\t\t\t\t\t<use href=\"#icon-assets\"></use>\n\t\t\t\t</svg>\n\t\t\t</span>\n\t\t\t{{ _(\"Move to Cart\") }}\n\t\t</button>\n\t{% else %}\n\t\t<div class=\"out-of-stock\">\n\t\t\t{{ _(\"Out of stock\") }}\n\t\t</div>\n\t{% endif %}\n</div>\n{%- endmacro -%}\n\n{%- macro ratings_with_title(avg_rating, title, size, rating_header_class, for_summary=False) -%}\n<div class=\"{{ 'd-flex' if not for_summary else '' }}\">\n\t<p class=\"mr-4 {{ rating_header_class }}\">\n\t\t<span>{{ _(title) }}</span>\n\t</p>\n\t<div class=\"rating {{ 'ratings-pill' if for_summary else ''}}\">\n\t\t{% for i in range(1,6) %}\n\t\t{% set fill_class = 'star-click' if i <= avg_rating*5 else '' %}\n\t\t\t<svg class=\"icon icon-{{ size }} {{ fill_class }}\">\n\t\t\t\t<use href=\"#icon-star\"></use>\n\t\t\t</svg>\n\t\t{% endfor %}\n\t</div>\n</div>\n{%- endmacro -%}\n\n{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=False, total_reviews=None)-%}\n<div class=\"rating-summary-section mt-4\">\n\t<div class=\"rating-summary-numbers col-3\">\n\t\t<h2 style=\"font-size: 2rem;\">\n\t\t\t{{ average_rating or 0 }}\n\t\t</h2>\n\t\t<div class=\"mb-2\" style=\"margin-top: -.5rem;\">\n\t\t\t{{ frappe.utils.cstr(total_reviews or 0) + \" \" + _(\"ratings\") }}\n\t\t</div>\n\n\t\t<!-- Ratings Summary -->\n\t\t{% if reviews %}\n\t\t\t{% set rating_title = frappe.utils.cstr(average_rating) + \" \" + _(\"out of 5\") if not for_summary else ''%}\n\t\t\t{{ ratings_with_title(average_whole_rating/5, rating_title, \"md\", \"rating-summary-title\", for_summary) }}\n\t\t{% endif %}\n\n\t\t<div class=\"mt-2\">{{ frappe.utils.cstr(average_rating or 0) + \" \" + _(\"out of 5\") }}</div>\n\t</div>\n\n\t<!-- Rating Progress Bars -->\n\t<div class=\"rating-progress-bar-section col-4 ml-4\">\n\t\t{% for percent in reviews_per_rating %}\n\t\t\t<div class=\"col-sm-4 small rating-bar-title\">\n\t\t\t\t{{ loop.index }} star\n\t\t\t</div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-md-7\">\n\t\t\t\t\t<div class=\"progress rating-progress-bar\" title=\"{{ percent }} % of reviews are {{ loop.index }} star\">\n\t\t\t\t\t\t<div class=\"progress-bar progress-bar-cosmetic\" role=\"progressbar\"\n\t\t\t\t\t\t\taria-valuenow=\"{{ percent }}\"\n\t\t\t\t\t\t\taria-valuemin=\"0\" aria-valuemax=\"100\"\n\t\t\t\t\t\t\tstyle=\"width: {{ percent }}%;\">\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-sm-1 small\">\n\t\t\t\t\t{{ percent }}%\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{% endfor %}\n\t</div>\n</div>\n{%- endmacro -%}\n\n{%- macro user_review(reviews)-%}\n<!-- User Reviews -->\n<div class=\"user-reviews\">\n\t{% for review in reviews %}\n\t\t<div class=\"mb-3 review\">\n\t\t\t{{ ratings_with_title(review.rating, _(review.review_title), \"sm\", \"user-review-title\") }}\n\t\t\t{% if review.comment %}\n\t\t\t\t<div class=\"product-description mb-4\">\n\t\t\t\t\t<p>\n\t\t\t\t\t\t{{ _(review.comment) }}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{% endif %}\n\t\t\t<div class=\"review-signature mb-2\">\n\t\t\t\t<span class=\"reviewer\">{{ _(review.customer) }}</span>\n\t\t\t\t<span class=\"indicator grey\" style=\"--text-on-gray: var(--gray-300);\"></span>\n\t\t\t\t<span class=\"reviewer\">{{ _(review.published_on) }}</span>\n\t\t\t</div>\n\t\t</div>\n\t{% endfor %}\n</div>\n{%- endmacro -%}\n\n{%- macro field_filter_section(filters)-%}\n{% for field_filter in filters %}\n\t{%- set item_field =  field_filter[0] %}\n\t{%- set values =  field_filter[1] %}\n\t<div class=\"mb-4 filter-block pb-5\">\n\t\t<div class=\"filter-label mb-3\">{{ _(item_field.label) }}</div>\n\n\t\t{% if values | len > 20 %}\n\t\t<!-- show inline filter if values more than 20 -->\n\t\t<input type=\"text\" class=\"form-control form-control-sm mb-2 filter-lookup-input\" placeholder=\"_('Search') {{ _(item_field.label) + 's' }}\"/>\n\t\t{% endif %}\n\n\t\t{% if values %}\n\t\t<div class=\"filter-options\">\n\t\t\t{% for value in values %}\n\t\t\t<div class=\"filter-lookup-wrapper checkbox\" data-value=\"{{ value }}\">\n\t\t\t\t<label for=\"{{value}}\">\n\t\t\t\t\t<input type=\"checkbox\"\n\t\t\t\t\t\tclass=\"product-filter field-filter\"\n\t\t\t\t\t\tid=\"{{value}}\"\n\t\t\t\t\t\tdata-filter-name=\"{{ item_field.fieldname }}\"\n\t\t\t\t\t\tdata-filter-value=\"{{ value }}\"\n\t\t\t\t\t\tstyle=\"width: 14px !important\">\n\t\t\t\t\t<span class=\"label-area\">{{ _(value) }}</span>\n\t\t\t\t</label>\n\t\t\t</div>\n\t\t\t{% endfor %}\n\t\t</div>\n\t\t{% else %}\n\t\t<i class=\"text-muted\">{{ _('No values') }}</i>\n\t\t{% endif %}\n\t</div>\n{% endfor %}\n{%- endmacro -%}\n\n{%- macro attribute_filter_section(filters)-%}\n{% for attribute in filters %}\n\t<div class=\"mb-4 filter-block pb-5\">\n\t\t<div class=\"filter-label mb-3\">{{ _(attribute.name) }}</div>\n\t\t{% if attribute.item_attribute_values | len > 20 %}\n\t\t<!-- show inline filter if values more than 20 -->\n\t\t<input type=\"text\" class=\"form-control form-control-sm mb-2 filter-lookup-input\" placeholder=\"_('Search') {{ _(attribute.name) + 's' }}\"/>\n\t\t{% endif %}\n\n\t\t{% if attribute.item_attribute_values %}\n\t\t<div class=\"filter-options\">\n\t\t\t{% for attr_value in attribute.item_attribute_values %}\n\t\t\t<div class=\"filter-lookup-wrapper checkbox\" data-value=\"{{ attr_value }}\">\n\t\t\t\t<label data-value=\"{{ attr_value }}\">\n\t\t\t\t\t<input type=\"checkbox\"\n\t\t\t\t\t\tclass=\"product-filter attribute-filter\"\n\t\t\t\t\t\tid=\"{{ attr_value }}\"\n\t\t\t\t\t\tdata-attribute-name=\"{{ attribute.name }}\"\n\t\t\t\t\t\tdata-attribute-value=\"{{ attr_value }}\"\n\t\t\t\t\t\tstyle=\"width: 14px !important\"\n\t\t\t\t\t\t{% if attr_value.checked %} checked {% endif %}>\n\t\t\t\t\t\t<span class=\"label-area\">{{ _(attr_value) }}</span>\n\t\t\t\t</label>\n\t\t\t</div>\n\t\t\t{% endfor %}\n\t\t</div>\n\t\t{% else %}\n\t\t<i class=\"text-muted\">{{ _('No values') }}</i>\n\t\t{% endif %}\n\t</div>\n{% endfor %}\n{%- endmacro -%}\n\n{%- macro recommended_item_row(item)-%}\n<div class=\"recommended-item mb-6 d-flex\">\n\t<div class=\"r-item-image\">\n\t\t{% if item.website_item_thumbnail %}\n\t\t\t{{ product_image(item.website_item_thumbnail, css_class=\"r-product-image\", alt=\"item.website_item_name\", no_border=True) }}\n\t\t{% else %}\n\t\t\t<div class=\"no-image-r-item\">\n\t\t\t\t{{ frappe.utils.get_abbr(item.website_item_name) or \"NA\" }}\n\t\t\t</div>\n\t\t{% endif %}\n\t</div>\n\t<div class=\"r-item-info\">\n\t\t<a href=\"/{{ item.route or '#'}}\" target=\"_blank\">\n\t\t\t{% set title = item.website_item_name %}\n\t\t\t{{ title[:70] + \"...\" if title|len > 70 else title }}\n\t\t</a>\n\n\t\t{% if item.get('price_info') %}\n\t\t\t{% set price = item.get('price_info') %}\n\t\t\t<div class=\"mt-2\">\n\t\t\t\t<span class=\"item-price\">\n\t\t\t\t\t{{ price.get('formatted_price') or '' }}\n\t\t\t\t</span>\n\n\t\t\t\t{% if price.get('formatted_mrp') %}\n\t\t\t\t\t<br>\n\t\t\t\t\t<span class=\"striked-item-price\">\n\t\t\t\t\t\t<s>MRP {{ _(price.formatted_mrp) }}</s>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"in-green\">\n\t\t\t\t\t\t- {{ price.get('formatted_discount_percent') or price.get('formatted_discount_rate')}}\n\t\t\t\t\t</span>\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t{% endif %}\n\t</div>\n</div>\n{%- endmacro -%}\n"
  },
  {
    "path": "webshop/templates/includes/navbar/navbar_items.html",
    "content": "{% extends 'frappe/templates/includes/navbar/navbar_items.html' %}\n\n{% block navbar_right_extension %}\n\t<li class=\"shopping-cart cart-icon hidden\">\n\t\t<a class=\"nav-link\" href=\"/cart\">\n\t\t\t<svg class=\"icon icon-lg\">\n\t\t\t\t<use href=\"#icon-assets\"></use>\n\t\t\t</svg>\n\t\t\t<span class=\"badge badge-primary shopping-badge\" id=\"cart-count\"></span>\n\t\t</a>\n\t</li>\n\t{% if frappe.db.get_single_value(\"Webshop Settings\", \"enable_wishlist\") %}\n\t\t<li class=\"wishlist wishlist-icon hidden\">\n\t\t\t<a class=\"nav-link\" href=\"/wishlist\">\n\t\t\t\t<svg class=\"icon icon-lg\">\n\t\t\t\t\t<use href=\"#icon-heart\"></use>\n\t\t\t\t</svg>\n\t\t\t\t<span class=\"badge badge-primary shopping-badge\" id=\"wish-count\"></span>\n\t\t\t</a>\n\t\t</li>\n\t{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/includes/order/order_macros.html",
    "content": "{% from \"webshop/templates/includes/macros.html\" import product_image %}\n\n{% macro item_name_and_description(d) %}\n\t<div class=\"row item_name_and_description\">\n\t\t<div class=\"col-xs-4 col-sm-2 order-image-col\">\n\t\t\t<div class=\"order-image h-100\">\n\t\t\t\t{% if d.thumbnail or d.image %}\n\t\t\t\t\t{{ product_image(d.thumbnail or d.image, no_border=True) }}\n\t\t\t\t{% else %}\n\t\t\t\t\t<div class=\"no-image-cart-item\" style=\"min-height: 100px;\">\n\t\t\t\t\t\t{{ frappe.utils.get_abbr(d.item_name) or \"NA\" }}\n\t\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"col-xs-8 col-sm-10\">\n\t\t\t{{ d.item_code }}\n\t\t\t<div class=\"text-muted small item-description\">\n\t\t\t\t{{ html2text(d.description) | truncate(140) }}\n\t\t\t</div>\n\t\t\t<span class=\"text-muted mt-2 d-l-n order-qty\">\n\t\t\t\t{{ _(\"Qty \") }}({{ d.get_formatted(\"qty\") }})\n\t\t\t</span>\n\t\t</div>\n\t</div>\n{% endmacro %}\n\n{% macro item_name_and_description_cart(d) %}\n\t<div class=\"row item_name_dropdown\">\n\t\t<div class=\"col-xs-4 col-sm-4 order-image-col\">\n\t\t\t<div class=\"order-image\">\n\t\t\t {{ product_image_square(d.thumbnail or d.image) }}\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"col-xs-8 col-sm-8\">\n\t\t   {{ d.item_name|truncate(25) }}\n\t\t\t<div class=\"input-group number-spinner\">\n\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t<button class=\"btn btn-light cart-btn\" data-dir=\"dwn\">\n\t\t\t\t\t\t–</button>\n\t\t\t\t</span>\n\t\t\t\t<input class=\"form-control text-right cart-qty\"\n\t\t\t\t\tvalue = \"{{ d.get_formatted('qty') }}\"\n\t\t\t\t\tdata-item-code=\"{{ d.item_code }}\">\n\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t<button class=\"btn btn-light cart-btn\" data-dir=\"up\">\n\t\t\t\t\t\t+</button>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{% endmacro %}\n"
  },
  {
    "path": "webshop/templates/includes/order/order_taxes.html",
    "content": "{% if doc.taxes %}\n\t<div class=\"w-100 order-taxes mt-5\">\n\t\t<div class=\"col-4 d-flex  border-btm pb-5\">\n\t\t\t<div class=\"item-grand-total col-8\">\n\t\t\t\t{{ _(\"Net Total\") }}\n\t\t\t</div>\n\t\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\") }}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{% endif %}\n\n{% for d in doc.taxes %}\n\t{% if d.tax_amount %}\n\t\t<div class=\"order-taxes w-100 mt-5\">\n\t\t\t<div class=\"col-4 d-flex  border-btm pb-5\">\n\t\t\t\t<div class=\"item-grand-total col-8\">\n\t\t\t\t\t{{ d.description }}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t\t\t{{ d.get_formatted(\"tax_amount\") }}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t{% endif %}\n{% endfor %}\n\n{% if doc.doctype == 'Quotation' %}\n\t{% if doc.coupon_code %}\n\t\t<div class=\"w-100 mt-5 order-taxes font-weight-bold\">\n\t\t\t<div class=\"col-4 d-flex  border-btm pb-5\">\n\t\t\t\t<div class=\"item-grand-total col-8\">\n\t\t\t\t\t{{ _(\"Savings\") }}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t\t\t{% set tot_quotation_discount = [] %}\n\t\t\t\t\t{%- for item in doc.items -%}\n\t\t\t\t\t\t{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)\n\t\t\t\t\t\t\t* item.discount_percentage) / 100)) %}\n\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }} </div>\n\t\t\t</div>\n\t\t</div>\n\t{% endif %}\n{% endif %}\n\n{% if doc.doctype == 'Sales Order' %}\n\t{% if doc.coupon_code %}\n\t\t<div class=\"w-100 order-taxes mt-5\">\n\t\t\t<div class=\"col-4 d-flex  border-btm pb-5\">\n\t\t\t\t<div class=\"item-grand-total col-8\">\n\t\t\t\t\t{{ _(\"Total Amount\") }}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t\t\t<span>\n\t\t\t\t\t\t{% set total_amount = [] %}\n\t\t\t\t\t\t{%- for item in doc.items -%}\n\t\t\t\t\t\t{% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}\n\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t{{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"order-taxes w-100 mt-5\">\n\t\t\t<div class=\"col-4 d-flex\">\n\t\t\t\t<div class=\"item-grand-total col-8\">\n\t\t\t\t\t{{ _(\"Applied Coupon Code\") }}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t\t\t<span>\n\t\t\t\t\t\t{%- for row in frappe.get_all(doctype=\"Coupon Code\",\n\t\t\t\t\t\tfields=[\"coupon_code\"], filters={ \"name\":doc.coupon_code}) -%}\n\t\t\t\t\t\t\t<span>{{ row.coupon_code }}</span>\n\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"order-taxes mt-5\">\n\t\t\t<div class=\"col-4 d-flex border-btm pb-5\">\n\t\t\t\t<div class=\"item-grand-total col-8\">\n\t\t\t\t\t{{ _(\"Savings\") }}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t\t\t<span>\n\t\t\t\t\t\t{% set tot_SO_discount = [] %}\n\t\t\t\t\t\t{%- for item in doc.items -%}\n\t\t\t\t\t\t{% if tot_SO_discount.append((((item.price_list_rate * item.qty)\n\t\t\t\t\t\t* item.discount_percentage) / 100)) %}{% endif %}\n\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t{% endif %}\n{% endif %}\n\n<div class=\"w-100 mt-5 order-taxes font-weight-bold\">\n\t<div class=\"col-4 d-flex\">\n\t\t<div class=\"item-grand-total col-8\">\n\t\t\t{{ _(\"Grand Total\") }}\n\t\t</div>\n\t\t<div class=\"item-grand-total col-4 text-right pr-0\">\n\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "webshop/templates/includes/product_page.js",
    "content": "// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n// License: GNU General Public License v3. See license.txt\n\nfrappe.ready(function() {\n\twindow.item_code = $('[itemscope] [itemprop=\"productID\"]').text().trim();\n\tvar qty = 0;\n\n\tfrappe.call({\n\t\ttype: \"POST\",\n\t\tmethod: \"webshop.webshop.shopping_cart.product_info.get_product_info_for_website\",\n\t\targs: {\n\t\t\titem_code: get_item_code()\n\t\t},\n\t\tcallback: function(r) {\n\t\t\tif(r.message) {\n\t\t\t\tif(r.message.cart_settings.enabled) {\n\t\t\t\t\tlet hide_add_to_cart = !r.message.product_info.price\n\t\t\t\t\t\t|| (!r.message.product_info.in_stock && !r.message.cart_settings.allow_items_not_in_stock);\n\t\t\t\t\t$(\".item-cart, .item-price, .item-stock\").toggleClass('hide', hide_add_to_cart);\n\t\t\t\t}\n\t\t\t\tif(r.message.cart_settings.show_price) {\n\t\t\t\t\t$(\".item-price\").toggleClass(\"hide\", false);\n\t\t\t\t}\n\t\t\t\tif(r.message.cart_settings.show_stock_availability) {\n\t\t\t\t\t$(\".item-stock\").toggleClass(\"hide\", false);\n\t\t\t\t}\n\t\t\t\tif(r.message.product_info.price) {\n\t\t\t\t\t$(\".item-price\")\n\t\t\t\t\t\t.html(r.message.product_info.price.formatted_price_sales_uom + \"<div style='font-size: small'>\\\n\t\t\t\t\t\t\t(\" + r.message.product_info.price.formatted_price + \" / \" + r.message.product_info.uom + \")</div>\");\n\n\t\t\t\t\tif(r.message.product_info.in_stock===0) {\n\t\t\t\t\t\t$(\".item-stock\").html(\"<div style='color: red'> <i class='fa fa-close'></i> {{ _(\"Not in stock\") }}</div>\");\n\t\t\t\t\t}\n\t\t\t\t\telse if(r.message.product_info.in_stock===1 && r.message.cart_settings.show_stock_availability) {\n\t\t\t\t\t\tvar qty_display = \"{{ _(\"In stock\") }}\";\n\t\t\t\t\t\tif (r.message.product_info.show_stock_qty) {\n\t\t\t\t\t\t\tqty_display += \" (\"+r.message.product_info.stock_qty+\")\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\t$(\".item-stock\").html(\"<div style='color: green'>\\\n\t\t\t\t\t\t\t<i class='fa fa-check'></i> \"+qty_display+\"</div>\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif(r.message.product_info.qty) {\n\t\t\t\t\t\tqty = r.message.product_info.qty;\n\t\t\t\t\t\ttoggle_update_cart(r.message.product_info.qty);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttoggle_update_cart(0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n\n\t$(\"#item-add-to-cart button\").on(\"click\", function() {\n\t\tfrappe.provide('webshop.shopping_cart');\n\n\t\twebshop.shopping_cart.update_cart({\n\t\t\titem_code: get_item_code(),\n\t\t\tqty: $(\"#item-spinner .cart-qty\").val(),\n\t\t\tcallback: function(r) {\n\t\t\t\tif(!r.exc) {\n\t\t\t\t\ttoggle_update_cart(1);\n\t\t\t\t\tqty = 1;\n\t\t\t\t}\n\t\t\t},\n\t\t\tbtn: this,\n\t\t});\n\t});\n\n\t$(\"#item-spinner\").on('click', '.number-spinner button', function () {\n\t\tvar btn = $(this),\n\t\t\tinput = btn.closest('.number-spinner').find('input'),\n\t\t\toldValue = input.val().trim(),\n\t\t\tnewVal = 0;\n\n\t\tif (btn.attr('data-dir') == 'up') {\n\t\t\tnewVal = Number.parseInt(oldValue) + 1;\n\t\t} else if (btn.attr('data-dir') == 'dwn')  {\n\t\t\tif (Number.parseInt(oldValue) > 1) {\n\t\t\t\tnewVal = Number.parseInt(oldValue) - 1;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tnewVal = Number.parseInt(oldValue);\n\t\t\t}\n\t\t}\n\t\tinput.val(newVal);\n\t});\n\n\t$(\"[itemscope] .item-view-attribute .form-control\").on(\"change\", function() {\n\t\ttry {\n\t\t\tvar item_code = encodeURIComponent(get_item_code());\n\n\t\t} catch(e) {\n\t\t\t// unable to find variant\n\t\t\t// then chose the closest available one\n\n\t\t\tvar attribute = $(this).attr(\"data-attribute\");\n\t\t\tvar attribute_value = $(this).val();\n\t\t\tvar item_code = find_closest_match(attribute, attribute_value);\n\n\t\t\tif (!item_code) {\n\t\t\t\tfrappe.msgprint(__(\"Cannot find a matching Item. Please select some other value for {0}.\", [attribute]))\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\n\t\tif (window.location.search == (\"?variant=\" + item_code) || window.location.search.includes(item_code)) {\n\t\t\treturn;\n\t\t}\n\n\t\twindow.location.href = window.location.pathname + \"?variant=\" + item_code;\n\t});\n\n\t// change the item image src when alternate images are hovered\n\t$(document.body).on('mouseover', '.item-alternative-image', (e) => {\n\t\tconst $alternative_image = $(e.currentTarget);\n\t\tconst src = $alternative_image.find('img').prop('src');\n\t\t$('.item-image img').prop('src', src);\n\t});\n});\n\nvar toggle_update_cart = function(qty) {\n\t$(\"#item-add-to-cart\").toggle(qty ? false : true);\n\t$(\"#item-update-cart\")\n\t\t.toggle(qty ? true : false)\n\t\t.find(\"input\").val(qty);\n\t$(\"#item-spinner\").toggle(qty ? false : true);\n}\n\nfunction get_item_code() {\n\tvar variant_info = window.variant_info;\n\tif(variant_info) {\n\t\tvar attributes = get_selected_attributes();\n\t\tvar no_of_attributes = Object.keys(attributes).length;\n\n\t\tfor(var i in variant_info) {\n\t\t\tvar variant = variant_info[i];\n\n\t\t\tif (variant.attributes.length < no_of_attributes) {\n\t\t\t\t// the case when variant has less attributes than template\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar match = true;\n\t\t\tfor(var j in variant.attributes) {\n\t\t\t\tif(attributes[variant.attributes[j].attribute]\n\t\t\t\t\t!= variant.attributes[j].attribute_value\n\t\t\t\t) {\n\t\t\t\t\tmatch = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(match) {\n\t\t\t\treturn variant.name;\n\t\t\t}\n\t\t}\n\t\tthrow \"Unable to match variant\";\n\t} else {\n\t\treturn window.item_code;\n\t}\n}\n\nfunction find_closest_match(selected_attribute, selected_attribute_value) {\n\t// find the closest match keeping the selected attribute in focus and get the item code\n\n\tvar attributes = get_selected_attributes();\n\n\tvar previous_match_score = 0;\n\tvar previous_no_of_attributes = 0;\n\tvar matched;\n\n\tvar variant_info = window.variant_info;\n\tfor(var i in variant_info) {\n\t\tvar variant = variant_info[i];\n\t\tvar match_score = 0;\n\t\tvar has_selected_attribute = false;\n\n\t\tfor(var j in variant.attributes) {\n\t\t\tif(attributes[variant.attributes[j].attribute]===variant.attributes[j].attribute_value) {\n\t\t\t\tmatch_score = match_score + 1;\n\n\t\t\t\tif (variant.attributes[j].attribute==selected_attribute && variant.attributes[j].attribute_value==selected_attribute_value) {\n\t\t\t\t\thas_selected_attribute = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (has_selected_attribute\n\t\t\t&& ((match_score > previous_match_score) || (match_score==previous_match_score && previous_no_of_attributes < variant.attributes.length))) {\n\t\t\tprevious_match_score = match_score;\n\t\t\tmatched = variant;\n\t\t\tprevious_no_of_attributes = variant.attributes.length;\n\n\n\t\t}\n\t}\n\n\tif (matched) {\n\t\tfor (var j in matched.attributes) {\n\t\t\tvar attr = matched.attributes[j];\n\t\t\t$('[itemscope]')\n\t\t\t\t.find(repl('.item-view-attribute .form-control[data-attribute=\"%(attribute)s\"]', attr))\n\t\t\t\t.val(attr.attribute_value);\n\t\t}\n\n\t\treturn matched.name;\n\t}\n}\n\nfunction get_selected_attributes() {\n\tvar attributes = {};\n\t$('[itemscope]').find(\".item-view-attribute .form-control\").each(function() {\n\t\tattributes[$(this).attr('data-attribute')] = $(this).val();\n\t});\n\treturn attributes;\n}\n"
  },
  {
    "path": "webshop/templates/pages/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/templates/pages/cart.html",
    "content": "{% extends \"templates/web.html\" %}\n\n{% block title %} {{ _(\"Shopping Cart\") }} {% endblock %}\n\n{% block header %}<h3 class=\"shopping-cart-header mt-2 mb-6\">{{ _(\"Shopping Cart\") }}</h1>{% endblock %}\n\n{% block header_actions %}\n{% endblock %}\n\n{% block page_content %}\n\n{% from \"templates/includes/macros.html\" import item_name_and_description %}\n\n{% if doc.items %}\n<div class=\"cart-container\">\n\t<div class=\"row m-0\">\n\t\t<!-- Left section -->\n\t\t<div class=\"col-md-8\">\n\t\t\t<div class=\"frappe-card p-5 mb-4\">\n\t\t\t\t<div id=\"cart-error\" class=\"alert alert-danger\" style=\"display: none;\"></div>\n\t\t\t\t<div class=\"cart-items-header\">\n\t\t\t\t\t{{ _('Items') }}\n\t\t\t\t</div>\n\t\t\t\t<table class=\"table mt-3 cart-table\">\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th class=\"item-column\">{{ _('Item') }}</th>\n\t\t\t\t\t\t\t<th width=\"20%\">{{ _('Quantity') }}</th>\n\t\t\t\t\t\t\t{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}\n\t\t\t\t\t\t\t\t<th width=\"20\" class=\"text-right column-sm-view\">{{ _('Subtotal') }}</th>\n\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t<th width=\"10%\" class=\"column-sm-view\"></th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody class=\"cart-items\">\n\t\t\t\t\t\t{% include \"templates/includes/cart/cart_items.html\" %}\n\t\t\t\t\t</tbody>\n\n\t\t\t\t\t{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}\n\t\t\t\t\t\t<tfoot class=\"cart-tax-items\">\n\t\t\t\t\t\t\t{% include \"templates/includes/cart/cart_items_total.html\" %}\n\t\t\t\t\t\t</tfoot>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</table>\n\n\t\t\t\t<div class=\"row mt-2\">\n\t\t\t\t\t<div class=\"col-3\">\n\t\t\t\t\t\t{% if cart_settings.enable_checkout %}\n\t\t\t\t\t\t\t<a class=\"btn btn-primary-light font-md\" href=\"/orders\">\n\t\t\t\t\t\t\t\t{{ _('Past Orders') }}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t{% else %}\n\t\t\t\t\t\t\t<a class=\"btn btn-primary-light font-md\" href=\"/quotations\">\n\t\t\t\t\t\t\t\t{{ _('Past Quotes') }}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-9\">\n\t\t\t\t\t\t{% if doc.items %}\n\t\t\t\t\t\t<div class=\"place-order-container\">\n\t\t\t\t\t\t\t<a class=\"btn btn-primary-light mr-2 font-md\" href=\"/all-products\">\n\t\t\t\t\t\t\t\t{{ _('Continue Shopping') }}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Terms and Conditions -->\n\t\t\t{% if doc.items %}\n\t\t\t\t{% if doc.terms %}\n\t\t\t\t\t<div class=\"t-and-c-container mt-4 frappe-card\">\n\t\t\t\t\t\t<h5>{{ _(\"Terms and Conditions\") }}</h5>\n\t\t\t\t\t\t<div class=\"t-and-c-terms mt-2\">\n\t\t\t\t\t\t\t{{ doc.terms }}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t</div>\n\n\t\t<!-- Right section -->\n\t\t<div class=\"col-md-4\">\n\t\t\t<div class=\"cart-payment-addresses\">\n\t\t\t\t<!-- Apply Coupon Code  -->\n\t\t\t\t{% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}\n\t\t\t\t{% set coupon_code = doc.coupon_code if doc.coupon_code else \"\" %}\n\n\t\t\t\t{% if show_coupon_code == 1%}\n\t\t\t\t\t{% if coupon_code %}\n\t\t\t\t\t\t{% include \"templates/includes/cart/coupon_code.html\" %}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t<div class=\"mb-3\">\n\t\t\t\t\t\t<div class=\"d-flex w-100\">\n\t\t\t\t\t\t\t<input type=\"text\" class=\"txtcoupon form-control mr-3 flex-grow-1 font-md\" placeholder=\"{{ _(\"Enter coupon code\") }}\" name=\"txtcouponcode\"  ></input>\n\t\t\t\t\t\t\t<button class=\"btn btn-secondary btn-sm bt-coupon font-md flex-shrink-0\">{{ _(\"Apply\") }}</button>\n\t\t\t\t\t\t\t<input type=\"hidden\" class=\"txtreferral_sales_partner font-md\" placeholder=\"Enter Sales Partner\" name=\"txtreferral_sales_partner\" type=\"text\"></input>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t{% endif %}\n\n\t\t\t\t<div class=\"mb-3 frappe-card p-5\">\n\t\t\t\t\t<div class=\"payment-summary\">\n\t\t\t\t\t\t{% include \"templates/includes/cart/cart_payment_summary.html\" %}\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"place-order\">\n\t\t\t\t\t\t{% include \"templates/includes/cart/place_order.html\" %}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{% include \"templates/includes/cart/cart_address.html\" %}\n\t\t\t</div>\n\t\t</div>\n\t\t{% endif %}\n\t</div>\n</div>\n{% else %}\n<div class=\"cart-empty frappe-card\">\n\t<div class=\"cart-empty-state\">\n\t\t<img src=\"/assets/webshop/images/cart-empty-state.png\" alt=\"Empty State\">\n\t</div>\n\t<div class=\"cart-empty-message mt-4\">{{ _('Your cart is Empty') }}</p>\n\t{% if cart_settings.enable_checkout %}\n\t\t<a class=\"btn btn-outline-primary\" href=\"/orders\" style=\"font-size: 16px;\">\n\t\t\t{{ _('See past orders') }}\n\t\t</a>\n\t\t{% else %}\n\t\t<a class=\"btn btn-outline-primary\" href=\"/quotations\" style=\"font-size: 16px;\">\n\t\t\t{{ _('See past quotations') }}\n\t\t</a>\n\t{% endif %}\n</div>\n{% endif %}\n\n{% endblock %}\n\n{% block base_scripts %}\n<!-- js should be loaded in body! -->\n{{ include_script(\"frappe-web.bundle.js\") }}\n{{ include_script(\"controls.bundle.js\") }}\n{{ include_script(\"dialog.bundle.js\") }}\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/pages/cart.js",
    "content": "// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n// License: GNU General Public License v3. See license.txt\n\n// JS exclusive to /cart page\nfrappe.provide(\"webshop.webshop.shopping_cart\");\nvar shopping_cart = webshop.webshop.shopping_cart;\n\n$.extend(shopping_cart, {\n\tshow_error: function(title, text) {\n\t\t$(\"#cart-container\").html('<div class=\"msg-box\"><h4>' +\n\t\t\ttitle + '</h4><p class=\"text-muted\">' + text + '</p></div>');\n\t},\n\n\tbind_events: function() {\n\t\tshopping_cart.bind_place_order();\n\t\tshopping_cart.bind_request_quotation();\n\t\tshopping_cart.bind_change_qty();\n\t\tshopping_cart.bind_remove_cart_item();\n\t\tshopping_cart.bind_change_notes();\n\t\tshopping_cart.bind_coupon_code();\n\t\tshopping_cart.bind_remove_coupon_code();\n\t},\n\n\tbind_place_order: function() {\n\t\t$(\".btn-place-order\").on(\"click\", function() {\n\t\t\tshopping_cart.place_order(this);\n\t\t});\n\t},\n\n\tbind_request_quotation: function() {\n\t\t$('.btn-request-for-quotation').on('click', function() {\n\t\t\tshopping_cart.request_quotation(this);\n\t\t});\n\t},\n\n\tbind_change_qty: function() {\n\t\t// bind update button\n\t\t$(\".cart-items\").on(\"change\", \".cart-qty\", function() {\n\t\t\tvar item_code = $(this).attr(\"data-item-code\");\n\t\t\tvar newVal = $(this).val();\n\t\t\tshopping_cart.shopping_cart_update({item_code, qty: newVal});\n\t\t});\n\n\t\t$(\".cart-items\").on('click', '.number-spinner button', function () {\n\t\t\tvar btn = $(this),\n\t\t\t\tinput = btn.closest('.number-spinner').find('input'),\n\t\t\t\toldValue = input.val().trim(),\n\t\t\t\tnewVal = 0;\n\n\t\t\tif (btn.attr('data-dir') == 'up') {\n\t\t\t\tnewVal = parseInt(oldValue) + 1;\n\t\t\t} else {\n\t\t\t\tif (oldValue > 1) {\n\t\t\t\t\tnewVal = parseInt(oldValue) - 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\tinput.val(newVal);\n\n\t\t\tlet notes = input.closest(\"td\").siblings().find(\".notes\").text().trim();\n\t\t\tvar item_code = input.attr(\"data-item-code\");\n\t\t\tshopping_cart.shopping_cart_update({\n\t\t\t\titem_code,\n\t\t\t\tqty: newVal,\n\t\t\t\tadditional_notes: notes\n\t\t\t});\n\t\t});\n\t},\n\n\tbind_change_notes: function() {\n\t\t$('.cart-items').on('change', 'textarea', function() {\n\t\t\tconst $textarea = $(this);\n\t\t\tconst item_code = $textarea.attr('data-item-code');\n\t\t\tconst qty = $textarea.closest('tr').find('.cart-qty').val();\n\t\t\tconst notes = $textarea.val();\n\t\t\tshopping_cart.shopping_cart_update({\n\t\t\t\titem_code,\n\t\t\t\tqty,\n\t\t\t\tadditional_notes: notes\n\t\t\t});\n\t\t});\n\t},\n\n\tbind_remove_cart_item: function() {\n\t\t$(\".cart-items\").on(\"click\", \".remove-cart-item\", (e) => {\n\t\t\tconst $remove_cart_item_btn = $(e.currentTarget);\n\t\t\tvar item_code = $remove_cart_item_btn.data(\"item-code\");\n\n\t\t\tshopping_cart.shopping_cart_update({\n\t\t\t\titem_code: item_code,\n\t\t\t\tqty: 0\n\t\t\t});\n\t\t});\n\t},\n\n\trender_tax_row: function($cart_taxes, doc, shipping_rules) {\n\t\tvar shipping_selector;\n\t\tif(shipping_rules) {\n\t\t\tshipping_selector = '<select class=\"form-control\">' + $.map(shipping_rules, function(rule) {\n\t\t\t\treturn '<option value=\"' + rule[0] + '\">' + rule[1] + '</option>' }).join(\"\\n\") +\n\t\t\t'</select>';\n\t\t}\n\n\t\tvar $tax_row = $(repl('<div class=\"row\">\\\n\t\t\t<div class=\"col-md-9 col-sm-9\">\\\n\t\t\t\t<div class=\"row\">\\\n\t\t\t\t\t<div class=\"col-md-9 col-md-offset-3\">' +\n\t\t\t\t\t(shipping_selector || '<p>%(description)s</p>') +\n\t\t\t\t\t'</div>\\\n\t\t\t\t</div>\\\n\t\t\t</div>\\\n\t\t\t<div class=\"col-md-3 col-sm-3 text-right\">\\\n\t\t\t\t<p' + (shipping_selector ? ' style=\"margin-top: 5px;\"' : \"\") + '>%(formatted_tax_amount)s</p>\\\n\t\t\t</div>\\\n\t\t</div>', doc)).appendTo($cart_taxes);\n\n\t\tif(shipping_selector) {\n\t\t\t$tax_row.find('select option').each(function(i, opt) {\n\t\t\t\tif($(opt).html() == doc.description) {\n\t\t\t\t\t$(opt).attr(\"selected\", \"selected\");\n\t\t\t\t}\n\t\t\t});\n\t\t\t$tax_row.find('select').on(\"change\", function() {\n\t\t\t\tshopping_cart.apply_shipping_rule($(this).val(), this);\n\t\t\t});\n\t\t}\n\t},\n\n\tapply_shipping_rule: function(rule, btn) {\n\t\treturn frappe.call({\n\t\t\tbtn: btn,\n\t\t\ttype: \"POST\",\n\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.apply_shipping_rule\",\n\t\t\targs: { shipping_rule: rule },\n\t\t\tcallback: function(r) {\n\t\t\t\tif(!r.exc) {\n\t\t\t\t\tshopping_cart.render(r.message);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\tplace_order: function(btn) {\n\t\tshopping_cart.freeze();\n\n\t\treturn frappe.call({\n\t\t\ttype: \"POST\",\n\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.place_order\",\n\t\t\tbtn: btn,\n\t\t\tcallback: function(r) {\n\t\t\t\tif(r.exc) {\n\t\t\t\t\tshopping_cart.unfreeze();\n\t\t\t\t\tvar msg = \"\";\n\t\t\t\t\tif(r._server_messages) {\n\t\t\t\t\t\tmsg = JSON.parse(r._server_messages || []).join(\"<br>\");\n\t\t\t\t\t}\n\n\t\t\t\t\t$(\"#cart-error\")\n\t\t\t\t\t\t.empty()\n\t\t\t\t\t\t.html(msg || frappe._(\"Something went wrong!\"))\n\t\t\t\t\t\t.toggle(true);\n\t\t\t\t} else {\n\t\t\t\t\t$(btn).hide();\n\t\t\t\t\twindow.location.href = '/orders/' + encodeURIComponent(r.message);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\trequest_quotation: function(btn) {\n\t\tshopping_cart.freeze();\n\n\t\treturn frappe.call({\n\t\t\ttype: \"POST\",\n\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.request_for_quotation\",\n\t\t\tbtn: btn,\n\t\t\tcallback: function(r) {\n\t\t\t\tif(r.exc) {\n\t\t\t\t\tshopping_cart.unfreeze();\n\t\t\t\t\tvar msg = \"\";\n\t\t\t\t\tif(r._server_messages) {\n\t\t\t\t\t\tmsg = JSON.parse(r._server_messages || []).join(\"<br>\");\n\t\t\t\t\t}\n\n\t\t\t\t\t$(\"#cart-error\")\n\t\t\t\t\t\t.empty()\n\t\t\t\t\t\t.html(msg || frappe._(\"Something went wrong!\"))\n\t\t\t\t\t\t.toggle(true);\n\t\t\t\t} else {\n\t\t\t\t\t$(btn).hide();\n\t\t\t\t\twindow.location.href = '/quotations/' + encodeURIComponent(r.message);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\tbind_coupon_code: function() {\n\t\t$(\".bt-coupon\").on(\"click\", function() {\n\t\t\tshopping_cart.apply_coupon_code(this);\n\t\t});\n\t},\n\n\tapply_coupon_code: function(btn) {\n\t\treturn frappe.call({\n\t\t\ttype: \"POST\",\n\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.apply_coupon_code\",\n\t\t\tbtn: btn,\n\t\t\targs : {\n\t\t\t\tapplied_code : $('.txtcoupon').val(),\n\t\t\t\tapplied_referral_sales_partner: $('.txtreferral_sales_partner').val()\n\t\t\t},\n\t\t\tcallback: function(r) {\n\t\t\t\tif (r && r.message){\n\t\t\t\t\tlocation.reload();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\tbind_remove_coupon_code: function() {\n\t\t$(\".bt-remove-coupon-code\").on(\"click\", function() {\n\t\t\tshopping_cart.remove_coupon_code(this);\n\t\t});\n\t},\n\tremove_coupon_code: function(btn) {\n\t\treturn frappe.call({\n\t\t\ttype: \"POST\",\n\t\t\tmethod: \"webshop.webshop.shopping_cart.cart.remove_coupon_code\",\n\t\t\tbtn: btn,\n\t\t\tcallback: function(r) {\n\t\t\t\tif (r && r.message){\n\t\t\t\t\tlocation.reload();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n});\n\nfrappe.ready(function() {\n\tif (window.location.pathname === \"/cart\") {\n\t\t$(\".cart-icon\").hide();\n\t}\n\tshopping_cart.parent = $(\".cart-container\");\n\tshopping_cart.bind_events();\n});\n\nfunction show_terms() {\n\tvar html = $(\".cart-terms\").html();\n\tfrappe.msgprint(html);\n}\n"
  },
  {
    "path": "webshop/templates/pages/cart.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nno_cache = 1\n\nfrom webshop.webshop.shopping_cart.cart import get_cart_quotation\n\n\ndef get_context(context):\n\tcontext.body_class = \"product-page\"\n\tcontext.update(get_cart_quotation())\n"
  },
  {
    "path": "webshop/templates/pages/customer_reviews.html",
    "content": "{% extends \"templates/web.html\" %}\n{% from \"webshop/templates/includes/macros.html\" import user_review, ratings_summary %}\n\n{% block title %} {{ _(\"Customer Reviews\") }} {% endblock %}\n\n{% block page_content %}\n<div class=\"product-container reviews-full-page col-md-12\">\n\t{% if enable_reviews %}\n\t\t<!-- Title and Action -->\n\t\t<div class=\"w-100 mb-6 d-flex\">\n\t\t\t<div class=\"reviews-header col-9\">\n\t\t\t\t{{ _(\"Customer Reviews\") }}\n\t\t\t</div>\n\n\t\t\t<div class=\"write-a-review-btn col-3\">\n\t\t\t\t<!-- Write a Review for legitimate users -->\n\t\t\t\t{% if frappe.session.user != \"Guest\" and user_is_customer %}\n\t\t\t\t\t<button class=\"btn btn-write-review\"\n\t\t\t\t\t\tdata-web-item=\"{{ web_item }}\">\n\t\t\t\t\t\t{{ _(\"Write a Review\") }}\n\t\t\t\t\t</button>\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Summary -->\n\t\t{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}\n\n\n\t\t<!-- Reviews and Comments -->\n\t\t<div class=\"mt-8\">\n\t\t\t{% if reviews %}\n\t\t\t\t{{ user_review(reviews) }}\n\n\t\t\t\t{% if not reviews | len >= total_reviews %}\n\t\t\t\t\t<button class=\"btn btn-light btn-view-more mr-2 mt-4 mb-4 w-30\"\n\t\t\t\t\t\tdata-web-item=\"{{ web_item }}\">\n\t\t\t\t\t\t{{ _(\"View More\") }}\n\t\t\t\t\t</button>\n\t\t\t\t{% endif %}\n\n\t\t\t{% else %}\n\t\t\t\t<h6 class=\"text-muted mt-6\">\n\t\t\t\t\t{{ _(\"No Reviews\") }}\n\t\t\t\t</h6>\n\t\t\t{% endif %}\n\t\t</div>\n\t{% else %}\n\t\t<!-- If reviews are disabled -->\n\t\t<div class=\"text-center\">\n\t\t\t<h3 class=\"text-muted mt-8\">\n\t\t\t\t{{ _(\"No Reviews\") }}\n\t\t\t</h3>\n\t\t</div>\n\t{% endif %}\n</div>\n\n{% endblock %}\n\n{% block base_scripts %}\n<!-- js should be loaded in body! -->\n<script type=\"text/javascript\" src=\"/assets/frappe/js/lib/jquery/jquery.min.js\"></script>\n<script type=\"text/javascript\" src=\"/assets/js/frappe-web.min.js\"></script>\n<script type=\"text/javascript\" src=\"/assets/js/control.min.js\"></script>\n<script type=\"text/javascript\" src=\"/assets/js/dialog.min.js\"></script>\n<script type=\"text/javascript\" src=\"/assets/js/bootstrap-4-web.min.js\"></script>\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/pages/customer_reviews.py",
    "content": "# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\nimport frappe\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n\tget_shopping_cart_settings,\n)\nfrom webshop.webshop.doctype.item_review.item_review import get_item_reviews\nfrom webshop.webshop.doctype.website_item.website_item import check_if_user_is_customer\n\n\ndef get_context(context):\n\tcontext.body_class = \"product-page\"\n\tcontext.no_cache = 1\n\tcontext.full_page = True\n\tcontext.reviews = None\n\n\tif frappe.form_dict and frappe.form_dict.get(\"web_item\"):\n\t\tcontext.web_item = frappe.form_dict.get(\"web_item\")\n\t\tcontext.user_is_customer = check_if_user_is_customer()\n\t\tcontext.enable_reviews = get_shopping_cart_settings().enable_reviews\n\n\t\tif context.enable_reviews:\n\t\t\treviews_data = get_item_reviews(context.web_item)\n\t\t\tcontext.update(reviews_data)\n"
  },
  {
    "path": "webshop/templates/pages/order.html",
    "content": "{% extends \"templates/web.html\" %}\n{% from \"webshop/templates/includes/order/order_macros.html\" import item_name_and_description %}\n\n{% block breadcrumbs %}\n\t{% include \"templates/includes/breadcrumbs.html\" %}\n{% endblock %}\n\n{% block title %}\n\t{{ doc.name }}\n{% endblock %}\n\n{% block header %}\n\t<h3 class=\"m-0\">{{ doc.name }}</h3>\n{% endblock %}\n\n{% block header_actions %}\n\t<div class=\"row\">\n\t\t<div class=\"dropdown\">\n\t\t\t<button class=\"btn btn-sm btn-secondary dropdown-toggle\" data-toggle=\"dropdown\" aria-expanded=\"false\">\n\t\t\t\t<span class=\"font-md\">{{ _('Actions') }}</span>\n\t\t\t\t<b class=\"caret\"></b>\n\t\t\t</button>\n\t\t\t<ul class=\"dropdown-menu dropdown-menu-right\" role=\"menu\">\n\t\t\t\t{% if doc.doctype == 'Purchase Order' and show_make_pi_button %}\n\t\t\t\t\t<a class=\"dropdown-item\"\n\t\t\t\t\t\thref=\"/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}\"\n\t\t\t\t\t\tdata-action=\"make_purchase_invoice\">{{ _(\"Make Purchase Invoice\") }}\n\t\t\t\t\t</a>\n\t\t\t\t{% endif %}\n\t\t\t\t<a class=\"dropdown-item\"\n\t\t\t\t\thref='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t\t{{ _(\"Print\") }}\n\t\t\t\t</a>\n\t\t\t</ul>\n\t\t</div>\n\t\t<div class=\"form-column col-sm-6\">\n\t\t\t<div class=\"page-header-actions-block\" data-html-block=\"header-actions\">\n\t\t\t\t<p>\n\t\t\t\t\t{% if enabled_checkout and (doc.doctype != \"Sales Invoice\" or doc.outstanding_amount > 0) %}\n\t\t\t\t\t<a href=\"/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart\"\n\t\t\t\t\t\tclass=\"btn btn-primary btn-sm\" id=\"pay-for-order\">\n\t\t\t\t\t\t{{ _(\"Pay\") }} {{doc.get_formatted(\"grand_total\") }}\n\t\t\t\t\t</a>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{% endblock %}\n\n{% block page_content %}\n\t<div>\n\t\t<div class=\"row transaction-subheading  mt-1\">\n\t\t\t<div class=\"col-6 text-muted small mt-1\">\n\t\t\t\t{{ frappe.utils.format_date(doc.transaction_date, 'medium') }}\n\t\t\t\t{% if doc.valid_till %}\n\t\t\t\t\t<p>\n\t\t\t\t\t\t{{ _(\"Valid Till\") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}\n\t\t\t\t\t</p>\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"row indicator-container mt-2\">\n\t\t\t<div class=\"col-10\">\n\t\t\t\t<span class=\"indicator-pill {{ doc.indicator_color or (\" blue\" if doc.docstatus==1 else \"darkgrey\" ) }}\">\n\t\t\t\t\t{% if doc.doctype == \"Quotation\" and not doc.docstatus %}\n\t\t\t\t\t\t{{ _(\"Pending\") }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t\t{{ _(doc.get('indicator_title')) or _(doc.status) or _(\"Submitted\") }}\n\t\t\t\t\t{% endif %}\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<div class=\"text-right col-2\">\n\t\t\t\t{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase\n\t\t\t\tOrder'] else doc.customer_name %}\n\t\t\t\t<b>{{ party_name }}</b>\n\n\t\t\t\t{% if doc.contact_display and doc.contact_display != party_name %}\n\t\t\t\t\t<br>\n\t\t\t\t\t{{ doc.contact_display }}\n\t\t\t\t{% endif %}\n\t\t\t</div>\n\t\t</div>\n\n\t\t{% if doc._header %}\n\t\t\t{{ doc._header }}\n\t\t{% endif %}\n\n\t\t<div class=\"order-container mt-4\">\n\t\t\t<!-- items -->\n\t\t\t<div class=\"w-100\">\n\t\t\t\t<div class=\"order-items order-item-header mb-1 row text-muted\">\n\t\t\t\t\t<span class=\"col-5\">\n\t\t\t\t\t\t{{ _(\"Item\") }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"d-s-n col-3\">\n\t\t\t\t\t\t{{ _(\"Quantity\") }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"col-2 pl-10\">\n\t\t\t\t\t\t{{ _(\"Rate\") }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"col-2 text-right\">\n\t\t\t\t\t\t{{ _(\"Amount\") }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t{% for d in doc.items %}\n\t\t\t\t<div class=\"order-items row align-items-center\">\n\t\t\t\t\t<span class=\"order-item-name col-5 pr-0\">\n\t\t\t\t\t\t{{ item_name_and_description(d) }}\n\t\t\t\t\t</span>\n\n\t\t\t\t\t<span class=\"d-s-n col-3 pl-10\">\n\t\t\t\t\t\t{{ d.get_formatted(\"qty\") }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"order-rate pl-4 col-2\">\n\t\t\t\t\t\t{{ d.get_formatted(\"rate\") }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"col-2 text-right\">\n\t\t\t\t\t\t{{ d.get_formatted(\"amount\") }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t{% endfor %}\n\t\t\t</div>\n\n\t\t\t<!-- taxes -->\n\t\t\t<div class=\"\">\n\t\t\t\t{% include \"webshop/templates/includes/order/order_taxes.html\" %}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t{% if enabled_checkout and ((doc.doctype==\"Sales Order\" and doc.per_billed <= 0)\n\t\tor (doc.doctype==\"Sales Invoice\" and doc.outstanding_amount> 0)) %}\n\t\t<div class=\"panel panel-default\">\n\t\t\t<div class=\"panel-collapse\">\n\t\t\t\t<div class=\"panel-body text-muted small\">\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t<div class=\"form-column col-sm-6\">\n\t\t\t\t\t\t\t{% if available_loyalty_points %}\n\t\t\t\t\t\t\t<div class=\"panel-heading\">\n\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t<div class=\"form-column col-sm-6 address-title\">\n\t\t\t\t\t\t\t\t\t\t<strong>Loyalty Points</strong>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t\t<div class=\"h6\">Enter Loyalty Points</div>\n\t\t\t\t\t\t\t\t<div class=\"control-input-wrapper\">\n\t\t\t\t\t\t\t\t\t<div class=\"control-input\">\n\t\t\t\t\t\t\t\t\t\t<input class=\"form-control\" type=\"number\" min=\"0\"\n\t\t\t\t\t\t\t\t\t\t\tmax=\"{{ available_loyalty_points }}\" id=\"loyalty-point-to-redeem\">\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p class=\"help-box small text-muted d-none d-sm-block\"> Available Points: {{\n\t\t\t\t\t\t\t\t\t\tavailable_loyalty_points }} </p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t{% endif %}\n\n\n\t{% if attachments %}\n\t\t<div class=\"order-item-table\">\n\t\t\t<div class=\"row order-items order-item-header text-muted\">\n\t\t\t\t<div class=\"col-sm-12 h6 text-uppercase\">\n\t\t\t\t\t{{ _(\"Attachments\") }}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"row order-items\">\n\t\t\t\t<div class=\"col-sm-12\">\n\t\t\t\t\t{% for attachment in attachments %}\n\t\t\t\t\t<p class=\"small\">\n\t\t\t\t\t\t<a href=\"{{ attachment.file_url }}\" target=\"blank\"> {{ attachment.file_name }} </a>\n\t\t\t\t\t</p>\n\t\t\t\t\t{% endfor %}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t{% endif %}\n\n\t{% if doc.terms %}\n\t\t<div class=\"terms-and-condition text-muted small\">\n\t\t\t<hr>\n\t\t\t<p>{{ doc.terms }}</p>\n\t\t</div>\n\t{% endif %}\n{% endblock %}\n\n{% block script %}\n\t<script> {% include \"templates/pages/order.js\" %}</script>\n\t<script>\n\t\twindow.doc_info = {\n\t\t\tcustomer: '{{doc.customer}}',\n\t\t\tdoctype: '{{ doc.doctype }}',\n\t\t\tdoctype_name: '{{ doc.name }}',\n\t\t\tgrand_total: '{{ doc.grand_total }}',\n\t\t\tcurrency: '{{ doc.currency }}'\n\t\t}\n\t</script>\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/pages/order.js",
    "content": "// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors\n// For license information, please see license.txt\n\nfrappe.ready(() => {\n\tvar loyalty_points_input = document.getElementById(\"loyalty-point-to-redeem\");\n\tvar loyalty_points_status = document.getElementById(\"loyalty-points-status\");\n\n\tif (loyalty_points_input) {\n\t\tloyalty_points_input.onblur = apply_loyalty_points;\n\t}\n\n\tfunction apply_loyalty_points() {\n\t\tvar loyalty_points = parseInt(loyalty_points_input.value);\n\n\t\tif (!loyalty_points) return;\n\n\t\tconst callback = async (r) => {\n\t\t\tif (!r) return;\n\n\t\t\tvar message = \"\"\n\t\t\tlet loyalty_amount = flt(r.message * loyalty_points);\n\n\t\t\tif (doc_info.grand_total && doc_info.grand_total < loyalty_amount) {\n\t\t\t\tlet redeemable_amount = parseInt(doc_info.grand_total/r.message);\n\t\t\t\tmessage = \"You can only redeem max \" + redeemable_amount + \" points in this order.\";\n\t\t\t\tfrappe.msgprint(__(message));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tmessage = loyalty_points + \" Loyalty Points of amount \"+ loyalty_amount + \" is applied.\"\n\t\t\tloyalty_points_status.innerHTML = message;\n\t\t\tfrappe.msgprint(__(message));\n\n\t\t\tconst args_obj = {\n\t\t\t\tdn: doc_info.doctype_name,\n\t\t\t\tdt: doc_info.doctype,\n\t\t\t\tsubmit_doc: 1,\n\t\t\t\torder_type: \"Shopping Cart\",\n\t\t\t\tloyalty_points,\n\t\t\t}\n\n\t\t\tconst payment_gateway_account = await frappe.db.get_single_value('Webshop Settings', 'payment_gateway_account')\n\n\t\t\tif (payment_gateway_account) {\n\t\t\t\targs_obj.payment_gateway_account = payment_gateway_account;\n\t\t\t}\n\n\t\t\tconst args_str = Object\n\t\t\t\t.entries(args_obj)\n\t\t\t\t.map((e) => e[0] + \"=\" + e[1])\n\t\t\t\t.join(\"&\");\n\n\t\t\tconst href_base_url = \"/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request\"\n\t\t\tconst href = href_base_url + \"?\" + args_str;\n\n\t\t\tvar payment_button = document.getElementById(\"pay-for-order\");\n\t\t\tpayment_button.innerHTML = __(\"Pay Remaining\");\n\t\t\tpayment_button.href = href;\n\t\t}\n\n\t\tfrappe.call({\n\t\t\tmethod: \"erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor\",\n\t\t\targs: {\n\t\t\t\t\"customer\": doc_info.customer\n\t\t\t},\n\t\t\tcallback,\n\t\t});\n\t}\n})\n"
  },
  {
    "path": "webshop/templates/pages/order.py",
    "content": "# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nimport frappe\nfrom frappe import _\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import show_attachments\n\n\ndef get_context(context):\n\tcontext.no_cache = 1\n\tcontext.show_sidebar = True\n\tcontext.doc = frappe.get_doc(frappe.form_dict.doctype, frappe.form_dict.name)\n\tif hasattr(context.doc, \"set_indicator\"):\n\t\tcontext.doc.set_indicator()\n\n\tif show_attachments():\n\t\tcontext.attachments = get_attachments(frappe.form_dict.doctype, frappe.form_dict.name)\n\n\tcontext.parents = frappe.form_dict.parents\n\tcontext.title = frappe.form_dict.name\n\tcontext.payment_ref = frappe.db.get_value(\n\t\t\"Payment Request\", {\"reference_name\": frappe.form_dict.name}, \"name\"\n\t)\n\n\tcontext.enabled_checkout = frappe.get_doc(\"Webshop Settings\").enable_checkout\n\n\tdefault_print_format = frappe.db.get_value(\n\t\t\"Property Setter\",\n\t\tdict(property=\"default_print_format\", doc_type=frappe.form_dict.doctype),\n\t\t\"value\",\n\t)\n\tif default_print_format:\n\t\tcontext.print_format = default_print_format\n\telse:\n\t\tcontext.print_format = \"Standard\"\n\n\tif not frappe.has_website_permission(context.doc):\n\t\tfrappe.throw(_(\"Not Permitted\"), frappe.PermissionError)\n\n\tif context.doc.get(\"customer\"):\n\t\t# check for the loyalty program of the customer\n\t\tcustomer_loyalty_program = frappe.db.get_value(\n\t\t\t\"Customer\", context.doc.customer, \"loyalty_program\"\n\t\t)\n\t\tif customer_loyalty_program:\n\t\t\tfrom erpnext.accounts.doctype.loyalty_program.loyalty_program import (\n\t\t\t\tget_loyalty_program_details_with_points,\n\t\t\t)\n\n\t\t\tloyalty_program_details = get_loyalty_program_details_with_points(\n\t\t\t\tcontext.doc.customer, customer_loyalty_program\n\t\t\t)\n\t\t\tcontext.available_loyalty_points = int(loyalty_program_details.get(\"loyalty_points\"))\n\n\t# show Make Purchase Invoice button based on permission\n\tcontext.show_make_pi_button = frappe.has_permission(\"Purchase Invoice\", \"create\")\n\n\ndef get_attachments(dt, dn):\n\treturn frappe.get_all(\n\t\t\"File\",\n\t\tfields=[\"name\", \"file_name\", \"file_url\", \"is_private\"],\n\t\tfilters={\"attached_to_name\": dn, \"attached_to_doctype\": dt, \"is_private\": 0},\n\t)\n"
  },
  {
    "path": "webshop/templates/pages/product_search.html",
    "content": "{% extends \"templates/web.html\" %}\n\n{% block title %} {{ _(\"Product Search\") }} {% endblock %}\n\n{% block header %}<h2>{{ _(\"Product Search\") }}</h2>{% endblock %}\n\n{% block page_content %}\n<script>{% include \"templates/includes/product_list.js\" %}</script>\n\n<script>\nfrappe.ready(function() {\n\tvar txt = frappe.utils.get_url_arg(\"search\");\n\t$(\".search-results\").html('{{ _(\"Search results for\") + \": \" + html2text(frappe.form_dict.search or \"\") | e | trim }}');\n\twindow.search = txt;\n\twindow.start = 0;\n\twindow.get_product_list();\n});\n</script>\n\n<div class=\"product-search-content\">\n    <h3 class=\"search-results\">{{ _(\"Search Results\") }}</h3>\n\t<div id=\"search-list\" class=\"row\">\n\n\t</div>\n\t<div style=\"text-align: center;\">\n\t\t<div class=\"more-btn\"\n\t\t\tstyle=\"display: none; text-align: center;\">\n            <button class=\"btn btn-light\">{{ _(\"More...\") }}</button>\n\t\t</div>\n\t</div>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/pages/product_search.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nimport json\n\nimport frappe\nfrom frappe.utils import cint, cstr\nfrom redis.commands.search.query import Query\n\nfrom webshop.webshop.redisearch_utils import (\n\tWEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,\n\tWEBSITE_ITEM_INDEX,\n\tWEBSITE_ITEM_NAME_AUTOCOMPLETE,\n\tis_redisearch_enabled,\n)\nfrom webshop.webshop.shopping_cart.product_info import set_product_info_for_website\nfrom webshop.webshop.doctype.override_doctype.item_group import get_item_for_list_in_html\n\nno_cache = 1\n\n\ndef get_context(context):\n\tcontext.show_search = True\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_product_list(search=None, start=0, limit=12):\n\tdata = get_product_data(search, start, limit)\n\n\tfor item in data:\n\t\tset_product_info_for_website(item)\n\n\treturn [get_item_for_list_in_html(r) for r in data]\n\n\ndef get_product_data(search=None, start=0, limit=12):\n\t# limit = 12 because we show 12 items in the grid view\n\t# base query\n\tquery = \"\"\"\n\t\tSELECT\n\t\t\tweb_item_name, item_name, item_code, brand, route,\n\t\t\twebsite_image, thumbnail, item_group,\n\t\t\tdescription, web_long_description as website_description,\n\t\t\twebsite_warehouse, ranking\n\t\tFROM `tabWebsite Item`\n\t\tWHERE published = 1\n\t\t\"\"\"\n\n\t# search term condition\n\tif search:\n\t\tquery += \"\"\" and (item_name like %(search)s\n\t\t\t\tor web_item_name like %(search)s\n\t\t\t\tor brand like %(search)s\n\t\t\t\tor web_long_description like %(search)s)\"\"\"\n\t\tsearch = \"%\" + cstr(search) + \"%\"\n\n\t# order by\n\tquery += \"\"\" ORDER BY ranking desc, modified desc limit %s offset %s\"\"\" % (\n\t\tcint(limit),\n\t\tcint(start),\n\t)\n\n\treturn frappe.db.sql(query, {\"search\": search}, as_dict=1)  # nosemgrep\n\n\n@frappe.whitelist(allow_guest=True)\ndef search(query):\n\tproduct_results = product_search(query)\n\tcategory_results = get_category_suggestions(query)\n\n\treturn {\n\t\t\"product_results\": product_results.get(\"results\") or [],\n\t\t\"category_results\": category_results.get(\"results\") or [],\n\t}\n\n\n@frappe.whitelist(allow_guest=True)\ndef product_search(query, limit=10, fuzzy_search=True):\n\tsearch_results = {\"from_redisearch\": True, \"results\": []}\n\n\tif not is_redisearch_enabled():\n\t\t# Redisearch module not enabled\n\t\tsearch_results[\"from_redisearch\"] = False\n\t\tsearch_results[\"results\"] = get_product_data(query, 0, limit)\n\t\treturn search_results\n\n\tif not query:\n\t\treturn search_results\n\n\tredis = frappe.cache()\n\tquery = clean_up_query(query)\n\n\t# TODO: Check perf/correctness with Suggestions & Query vs only Query\n\t# TODO: Use Levenshtein Distance in Query (max=3)\n\tredisearch = redis.ft(WEBSITE_ITEM_INDEX)\n\tsuggestions = redisearch.sugget(\n\t\tWEBSITE_ITEM_NAME_AUTOCOMPLETE,\n\t\tquery,\n\t\tnum=limit,\n\t\tfuzzy=fuzzy_search and len(query) > 3,\n\t)\n\n\t# Build a query\n\tquery_string = query\n\n\tfor s in suggestions:\n\t\tquery_string += f\"|('{clean_up_query(s.string)}')\"\n\n\tq = Query(query_string)\n\tresults = redisearch.search(q)\n\n\tsearch_results[\"results\"] = list(map(convert_to_dict, results.docs))\n\tsearch_results[\"results\"] = sorted(\n\t\tsearch_results[\"results\"], key=lambda k: frappe.utils.cint(k[\"ranking\"]), reverse=True\n\t)\n\n\treturn search_results\n\n\ndef clean_up_query(query):\n\treturn \"\".join(c for c in query if c.isalnum() or c.isspace())\n\n\ndef convert_to_dict(redis_search_doc):\n\treturn redis_search_doc.__dict__\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_category_suggestions(query):\n\tsearch_results = {\"results\": []}\n\n\tif not is_redisearch_enabled():\n\t\t# Redisearch module not enabled, query db\n\t\tcategories = frappe.db.get_all(\n\t\t\t\"Item Group\",\n\t\t\tfilters={\"name\": [\"like\", \"%{0}%\".format(query)], \"show_in_website\": 1},\n\t\t\tfields=[\"name\", \"route\"],\n\t\t)\n\t\tsearch_results[\"results\"] = categories\n\t\treturn search_results\n\n\tif not query:\n\t\treturn search_results\n\n\tac = frappe.cache().ft()\n\tsuggestions = ac.sugget(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, query, num=10, with_payloads=True)\n\n\tresults = [json.loads(s.payload) for s in suggestions]\n\n\tsearch_results[\"results\"] = results\n\n\treturn search_results\n"
  },
  {
    "path": "webshop/templates/pages/wishlist.html",
    "content": "{% extends \"templates/web.html\" %}\n\n{% block title %} {{ _(\"Wishlist\") }} {% endblock %}\n\n{% block header %}<h3 class=\"shopping-cart-header mt-2 mb-6\">{{ _(\"Wishlist\") }}</h1>{% endblock %}\n\n{% block page_content %}\n{% if items %}\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12 item-card-group-section\">\n\t\t\t<div class=\"row products-list\">\n\t\t\t\t\t{% from \"webshop/templates/includes/macros.html\" import wishlist_card %}\n\t\t\t\t\t{% for item in items %}\n\t\t\t\t\t\t{{ wishlist_card(item, settings) }}\n\t\t\t\t\t{% endfor %}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{% else %}\n\t<div class=\"cart-empty frappe-card\">\n\t\t<div class=\"cart-empty-state\">\n\t\t\t<img src=\"/assets/webshop/images/cart-empty-state.png\" alt=\"Empty Cart\">\n\t\t</div>\n\t\t<div class=\"cart-empty-message mt-4\">{{ _('Wishlist is empty!') }}</p>\n\t</div>\n{% endif %}\n\n{% endblock %}\n"
  },
  {
    "path": "webshop/templates/pages/wishlist.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\nimport frappe\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n    get_shopping_cart_settings,\n)\nfrom webshop.webshop.shopping_cart.cart import _set_price_list\nfrom erpnext.utilities.product import get_price\nfrom webshop.webshop.shopping_cart.cart import get_party\n\n\ndef get_context(context):\n\tis_guest = frappe.session.user == \"Guest\"\n\n\tsettings = get_shopping_cart_settings()\n\titems = get_wishlist_items() if not is_guest else []\n\tselling_price_list = _set_price_list(settings) if not is_guest else None\n\n\titems = set_stock_price_details(items, settings, selling_price_list)\n\n\tcontext.body_class = \"product-page\"\n\tcontext.items = items\n\tcontext.settings = settings\n\tcontext.no_cache = 1\n\n\ndef get_stock_availability(item_code, warehouse):\n\tfrom erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses\n\n\tif warehouse and frappe.get_cached_value(\"Warehouse\", warehouse, \"is_group\") == 1:\n\t\twarehouses = get_child_warehouses(warehouse)\n\telse:\n\t\twarehouses = [warehouse] if warehouse else []\n\n\tstock_qty = 0.0\n\tfor warehouse in warehouses:\n\t\tstock_qty += frappe.utils.flt(\n\t\t\tfrappe.db.get_value(\"Bin\", {\"item_code\": item_code, \"warehouse\": warehouse}, \"actual_qty\")\n\t\t)\n\n\treturn bool(stock_qty)\n\n\ndef get_wishlist_items():\n\tif not frappe.db.exists(\"Wishlist\", frappe.session.user):\n\t\treturn []\n\n\treturn frappe.db.get_all(\n\t\t\"Wishlist Item\",\n\t\tfilters={\"parent\": frappe.session.user},\n\t\tfields=[\n\t\t\t\"web_item_name\",\n\t\t\t\"item_code\",\n\t\t\t\"item_name\",\n\t\t\t\"website_item\",\n\t\t\t\"warehouse\",\n\t\t\t\"image\",\n\t\t\t\"item_group\",\n\t\t\t\"route\",\n\t\t],\n\t)\n\n\ndef set_stock_price_details(items, settings, selling_price_list):\n\tfor item in items:\n\t\tif settings.show_stock_availability:\n\t\t\titem.available = get_stock_availability(\n\t\t\t\titem.item_code, item.get(\"warehouse\")\n\t\t\t)\n\n\t\tparty = get_party()\n\n\t\tprice_details = get_price(\n\t\t\titem.item_code,\n\t\t\tselling_price_list,\n\t\t\tsettings.default_customer_group,\n\t\t\tsettings.company,\n\t\t\tparty=party,\n\t\t)\n\n\t\tif price_details:\n\t\t\titem.formatted_price = price_details.get(\"formatted_price\")\n\t\t\titem.formatted_mrp = price_details.get(\"formatted_mrp\")\n\t\t\tif item.formatted_mrp:\n\t\t\t\titem.discount = price_details.get(\n\t\t\t\t\t\"formatted_discount_percent\"\n\t\t\t\t) or price_details.get(\"formatted_discount_rate\")\n\n\treturn items\n"
  },
  {
    "path": "webshop/webshop/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/api.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport json\n\nimport frappe\nfrom frappe.utils import cint\n\nfrom webshop.webshop.product_data_engine.filters import ProductFiltersBuilder\nfrom webshop.webshop.product_data_engine.query import ProductQuery\nfrom webshop.webshop.doctype.override_doctype.item_group import get_child_groups_for_website\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_product_filter_data(query_args=None):\n\t\"\"\"\n\tReturns filtered products and discount filters.\n\n\tArgs:\n\t\tquery_args (dict): contains filters to get products list\n\n\tQuery Args filters:\n\t\tsearch (str): Search Term.\n\t\tfield_filters (dict): Keys include item_group, brand, etc.\n\t\tattribute_filters(dict): Keys include Color, Size, etc.\n\t\tstart (int): Offset items by\n\t\titem_group (str): Valid Item Group\n\t\tfrom_filters (bool): Set as True to jump to page 1\n\t\"\"\"\n\tif isinstance(query_args, str):\n\t\tquery_args = json.loads(query_args)\n\n\tquery_args = frappe._dict(query_args or {})\n\n\tif query_args:\n\t\tsearch = query_args.get(\"search\")\n\t\tfield_filters = query_args.get(\"field_filters\", {})\n\t\tattribute_filters = query_args.get(\"attribute_filters\", {})\n\t\tstart = cint(query_args.start) if query_args.get(\"start\") else 0\n\t\titem_group = query_args.get(\"item_group\")\n\t\tfrom_filters = query_args.get(\"from_filters\")\n\telse:\n\t\tsearch, attribute_filters, item_group, from_filters = None, None, None, None\n\t\tfield_filters = {}\n\t\tstart = 0\n\n\t# if new filter is checked, reset start to show filtered items from page 1\n\tif from_filters:\n\t\tstart = 0\n\n\tsub_categories = []\n\tif item_group:\n\t\tsub_categories = get_child_groups_for_website(item_group, immediate=True)\n\n\tengine = ProductQuery()\n\n\ttry:\n\t\tresult = engine.query(\n\t\t\tattribute_filters,\n\t\t\tfield_filters,\n\t\t\tsearch_term=search,\n\t\t\tstart=start,\n\t\t\titem_group=item_group,\n\t\t)\n\texcept Exception:\n\t\tfrappe.log_error(\"Product query with filter failed\")\n\t\treturn {\"exc\": \"Something went wrong!\"}\n\n\t# discount filter data\n\tfilters = {}\n\tdiscounts = result[\"discounts\"]\n\n\tif discounts:\n\t\tfilter_engine = ProductFiltersBuilder()\n\t\tfilters[\"discount_filters\"] = filter_engine.get_discount_filters(discounts)\n\n\treturn {\n\t\t\"items\": result[\"items\"] or [],\n\t\t\"filters\": filters,\n\t\t\"settings\": engine.settings,\n\t\t\"sub_categories\": sub_categories,\n\t\t\"items_count\": result[\"items_count\"],\n\t}\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_guest_redirect_on_action():\n\treturn frappe.db.get_single_value(\"Webshop Settings\", \"redirect_on_action\")\n"
  },
  {
    "path": "webshop/webshop/crud_events/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/crud_events/item/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/crud_events/item/invalidate_item_variants_cache.py",
    "content": "import frappe\nfrom webshop.webshop.variant_selector.item_variants_cache import (\n    ItemVariantsCacheManager,\n)\n\n\ndef execute(doc, method=None, old_name=None, new_name=None, merge=False):\n    \"\"\"\n    Rebuild ItemVariantsCacheManager via Item or Website Item.\n    \"\"\"\n    item_code = None\n    is_web_item = doc.get(\"published_in_website\") or doc.get(\"published\")\n    is_published = frappe.db.get_value(\"Item\", doc.variant_of, \"published_in_website\")\n\n    if doc.has_variants and is_web_item:\n        item_code = doc.item_code\n\n    elif doc.variant_of and is_published:\n        item_code = doc.variant_of\n\n    if item_code:\n        item_cache = ItemVariantsCacheManager(item_code)\n        item_cache.rebuild_cache()\n"
  },
  {
    "path": "webshop/webshop/crud_events/item/update_website_item.py",
    "content": "import frappe\n\n\ndef execute(doc, method=None):\n    \"\"\"Update Website Item if change in Item impacts it.\"\"\"\n    web_item = frappe.db.exists(\"Website Item\", {\"item_code\": doc.item_code})\n\n    if web_item:\n        changed = {}\n        editable_fields = [\n            \"item_name\",\n            \"item_group\",\n            \"stock_uom\",\n            \"brand\",\n            \"description\",\n            \"disabled\",\n        ]\n        doc_before_save = doc.get_doc_before_save()\n\n        for field in editable_fields:\n            if doc_before_save.get(field) != doc.get(field):\n                if field == \"disabled\":\n                    changed[\"published\"] = not doc.get(field)\n                else:\n                    changed[field] = doc.get(field)\n\n                if not changed:\n                    return\n\n                web_item_doc = frappe.get_doc(\"Website Item\", web_item)\n                web_item_doc.update(changed)\n                web_item_doc.save()\n"
  },
  {
    "path": "webshop/webshop/crud_events/item/validate_duplicate_website_item.py",
    "content": "import frappe\n\nfrom frappe import _\nfrom frappe.utils import get_link_to_form\n\n\nclass DataValidationError(frappe.ValidationError):\n    pass\n\n\ndef execute(doc, method=None, old_name=None, new_name=None, merge=False):\n    \"\"\"\n    Block merge if both old and new items have website items against them.\n    This is to avoid duplicate website items after merging.\n    \"\"\"\n    if not merge:\n        return\n\n    web_items = frappe.get_all(\n        \"Website Item\",\n        filters={\"item_code\": [\"in\", [old_name, new_name]]},\n        fields=[\"item_code\", \"name\"],\n    )\n\n    if len(web_items) <= 1:\n        return\n\n    old_web_item = [d.get(\"name\") for d in web_items if d.get(\"item_code\") == old_name][0]\n    web_item_link = get_link_to_form(\"Website Item\", old_web_item)\n    old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)\n\n    msg = f\"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}\"\n    frappe.throw(_(msg), title=_(\"Cannot Merge\"), exc=DataValidationError)\n\n"
  },
  {
    "path": "webshop/webshop/crud_events/price_list/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/crud_events/price_list/check_impact_on_cart.py",
    "content": "import frappe\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n    validate_cart_settings,\n)\n\n\ndef execute(doc, method=None):\n    \"\"\"\n    Check if Price List currency change impacts Webshop Cart\n    \"\"\"\n    if doc.is_new():\n        return\n\n    doc_before_save = doc.get_doc_before_save()\n    currency_changed = doc.currency != doc_before_save.currency\n    affects_cart = doc.name == frappe.get_cached_value(\n        \"Webshop Settings\", None, \"price_list\"\n    )\n\n    if currency_changed and affects_cart:\n        validate_cart_settings()\n"
  },
  {
    "path": "webshop/webshop/crud_events/quotation/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/crud_events/quotation/validate_shopping_cart_items.py",
    "content": "import frappe\nfrom frappe import _\n\n\ndef execute(doc, method=None):\n    if doc.order_type != \"Shopping Cart\":\n        return\n\n    webshop_settings = frappe.get_cached_doc(\"Webshop Settings\")\n    for item in doc.items:\n        has_web_item = frappe.db.exists(\"Website Item\", {\"item_code\": item.item_code})\n\n        # If variant is unpublished but template is published: valid\n        template = frappe.get_cached_value(\"Item\", item.item_code, \"variant_of\")\n        if template and not has_web_item:\n            has_web_item = frappe.db.exists(\"Website Item\", {\"item_code\": template})\n\n        if not has_web_item and not webshop_settings.allow_non_website_items_in_cart_quotation:\n            frappe.throw(\n                _(\n                    \"Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations\"\n                ).format(item.idx, frappe.bold(item.item_code)),\n                title=_(\"Unpublished Item\"),\n            )\n"
  },
  {
    "path": "webshop/webshop/crud_events/tax_rule/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/crud_events/tax_rule/validate_use_for_cart.py",
    "content": "import frappe\nfrom frappe import _\nfrom frappe.utils import cint\n\n\ndef execute(doc, method=None):\n    \"\"\"\n    If shopping cart is enabled and no tax rule exists for shopping cart, enable this one\n    \"\"\"\n    if doc.use_for_shopping_cart:\n        return\n\n    is_enabled = cint(frappe.db.get_single_value(\"Webshop Settings\", \"enabled\"))\n\n    if not is_enabled:\n        return\n\n    use_for_cart = frappe.db.get_value(\n        \"Tax Rule\", {\"use_for_shopping_cart\": 1, \"name\": [\"!=\", doc.name]}\n    )\n\n    if not use_for_cart:\n        return\n\n    doc.use_for_shopping_cart = 1\n\n    frappe.msgprint(\n        _(\n            \"\"\"\n            Enabling 'Use for Shopping Cart', as Shopping Cart is enabled\n            and there should be at least one Tax Rule for Shopping Cart\n            \"\"\"\n        )\n    )\n"
  },
  {
    "path": "webshop/webshop/doctype/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/homepage_featured_product/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/homepage_featured_product/homepage_featured_product.json",
    "content": "{\n \"actions\": [],\n \"allow_rename\": 1,\n \"creation\": \"2023-12-14 22:14:23.853797\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"item_code\",\n  \"view\",\n  \"column_break_nxmx\",\n  \"item_name\",\n  \"section_break_qlos\",\n  \"description\",\n  \"column_break_jxff\",\n  \"image\",\n  \"thumbnail\",\n  \"route\"\n ],\n \"fields\": [\n  {\n   \"fieldname\": \"item_code\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 1\n  },\n  {\n   \"fieldname\": \"column_break_nxmx\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.item_name\",\n   \"fieldname\": \"item_name\",\n   \"fieldtype\": \"Data\",\n   \"in_list_view\": 1,\n   \"label\": \"Item Name\"\n  },\n  {\n   \"fieldname\": \"view\",\n   \"fieldtype\": \"Button\",\n   \"in_list_view\": 1,\n   \"label\": \"View\"\n  },\n  {\n   \"fieldname\": \"section_break_qlos\",\n   \"fieldtype\": \"Section Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.web_long_description\",\n   \"fieldname\": \"description\",\n   \"fieldtype\": \"Text Editor\",\n   \"in_list_view\": 1,\n   \"label\": \"Description\"\n  },\n  {\n   \"fieldname\": \"column_break_jxff\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.website_image\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"image\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Image\"\n  },\n  {\n   \"fetch_from\": \"item_code.thumbnail\",\n   \"fieldname\": \"thumbnail\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Thumbnail\"\n  },\n  {\n   \"fetch_from\": \"item_code.route\",\n   \"fieldname\": \"route\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Route\",\n   \"read_only\": 1\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"istable\": 1,\n \"links\": [],\n \"modified\": \"2023-12-14 22:33:25.457721\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Homepage Featured Product\",\n \"owner\": \"Administrator\",\n \"permissions\": [],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"states\": []\n}"
  },
  {
    "path": "webshop/webshop/doctype/homepage_featured_product/homepage_featured_product.py",
    "content": "# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\n# import frappe\nfrom frappe.model.document import Document\n\n\nclass HomepageFeaturedProduct(Document):\n\tpass\n"
  },
  {
    "path": "webshop/webshop/doctype/item_review/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/item_review/item_review.js",
    "content": "// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n// For license information, please see license.txt\n\nfrappe.ui.form.on('Item Review', {\n\t// refresh: function(frm) {\n\n\t// }\n});\n"
  },
  {
    "path": "webshop/webshop/doctype/item_review/item_review.json",
    "content": "{\n \"actions\": [],\n \"beta\": 1,\n \"creation\": \"2021-03-23 16:47:26.542226\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"website_item\",\n  \"user\",\n  \"customer\",\n  \"column_break_3\",\n  \"item\",\n  \"published_on\",\n  \"reviews_section\",\n  \"review_title\",\n  \"rating\",\n  \"comment\"\n ],\n \"fields\": [\n  {\n   \"fieldname\": \"website_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"read_only\": 1,\n   \"reqd\": 1\n  },\n  {\n   \"fieldname\": \"user\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"User\",\n   \"options\": \"User\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"column_break_3\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"website_item.item_code\",\n   \"fieldname\": \"item\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Item\",\n   \"options\": \"Item\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"reviews_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Reviews\"\n  },\n  {\n   \"fieldname\": \"rating\",\n   \"fieldtype\": \"Rating\",\n   \"in_list_view\": 1,\n   \"label\": \"Rating\"\n  },\n  {\n   \"fieldname\": \"comment\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Comment\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"review_title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Review Title\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"customer\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Customer\",\n   \"options\": \"Customer\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"published_on\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Published on\",\n   \"read_only\": 1\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"links\": [],\n \"modified\": \"2023-10-13 17:35:32.281964\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Item Review\",\n \"owner\": \"Administrator\",\n \"permissions\": [\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"System Manager\",\n   \"share\": 1,\n   \"write\": 1\n  },\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"Website Manager\",\n   \"share\": 1,\n   \"write\": 1\n  },\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"report\": 1,\n   \"role\": \"Customer\",\n   \"share\": 1\n  }\n ],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"states\": [],\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/item_review/item_review.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nfrom datetime import datetime\n\nimport frappe\nfrom frappe import _\nfrom frappe.contacts.doctype.contact.contact import get_contact_name\nfrom frappe.model.document import Document\nfrom frappe.utils import cint, flt\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n\tget_shopping_cart_settings,\n)\n\nfrom frappe.query_builder import DocType, functions\n\nclass UnverifiedReviewer(frappe.ValidationError):\n\tpass\n\n\nclass ItemReview(Document):\n\tdef after_insert(self):\n\t\t# regenerate cache on review creation\n\t\treviews_dict = get_queried_reviews(self.website_item)\n\t\tset_reviews_in_cache(self.website_item, reviews_dict)\n\n\tdef after_delete(self):\n\t\t# regenerate cache on review deletion\n\t\treviews_dict = get_queried_reviews(self.website_item)\n\t\tset_reviews_in_cache(self.website_item, reviews_dict)\n\n\n@frappe.whitelist()\ndef get_item_reviews(web_item, start=0, end=10, data=None):\n\t\"Get Website Item Review Data.\"\n\tstart, end = cint(start), cint(end)\n\tsettings = get_shopping_cart_settings()\n\n\t# Get cached reviews for first page (start=0)\n\t# avoid cache when page is different\n\tfrom_cache = not bool(start)\n\n\tif not data:\n\t\tdata = frappe._dict()\n\n\tif settings and settings.get(\"enable_reviews\"):\n\t\treviews_cache = frappe.cache().hget(\"item_reviews\", web_item)\n\t\tif from_cache and reviews_cache:\n\t\t\tdata = reviews_cache\n\t\telse:\n\t\t\tdata = get_queried_reviews(web_item, start, end, data)\n\t\t\tif from_cache:\n\t\t\t\tset_reviews_in_cache(web_item, data)\n\n\treturn data\n\n\ndef get_queried_reviews(web_item, start=0, end=10, data=None):\n\t\"\"\"\n\tQuery Website Item wise reviews and cache if needed.\n\tCache stores only first page of reviews i.e. 10 reviews maximum.\n\tReturns:\n\t        dict: Containing reviews, average ratings, % of reviews per rating and total reviews.\n\t\"\"\"\n\tif not data:\n\t\tdata = frappe._dict()\n\n\tdata.reviews = frappe.db.get_all(\n\t\t\"Item Review\",\n\t\tfilters={\"website_item\": web_item},\n\t\tfields=[\"*\"],\n\t\tlimit_start=start,\n\t\tlimit_page_length=end,\n\t)\n\n\treview = DocType(\"Item Review\")\n\n\ttry:\n\t\trating_data = frappe.db.get_all(\n\t\t\t\"Item Review\",\n\t\t\tfilters={\"website_item\": web_item},\n\t\t\tfields=[\n\t\t\t\tfunctions.Avg(review.rating * 5).as_(\"average\"),\n\t\t\t\t{\"COUNT\": \"*\", \"as\": \"total\"},\n\t\t\t]\n\t\t)[0]\n\texcept (TypeError, AttributeError):\n\t\trating_data = frappe.db.get_all(\n\t\t\t\"Item Review\",\n\t\t\tfilters={\"website_item\": web_item},\n\t\t\tfields=[\"avg(rating*5) as average, count(*) as total\"],\n\t\t)[0]\n\n\tdata.average_rating = flt(rating_data.average, 5)\n\tdata.average_whole_rating = flt(data.average_rating, 0)\n\n\t# get % of reviews per rating\n\treviews_per_rating = []\n\tfor i in range(1, 6):\n\t\ttry:\n\t\t\tcount = frappe.db.get_all(\n\t\t\t\"Item Review\", filters={\"website_item\": web_item, \"rating\": i/5}, fields=[{\"COUNT\": \"*\", \"as\": \"count\"}]\n\t\t)[0].count\n\t\texcept (TypeError, AttributeError):\n\t\t\tcount =  frappe.db.get_all(\n\t\t\t\"Item Review\", filters={\"website_item\": web_item, \"rating\": i/5}, fields=[\"count(*) as count\"]\n\t\t)[0].count\n\n\t\tpercent = flt((count / rating_data.total or 1) * 100, 0) if count else 0\n\t\treviews_per_rating.append(percent)\n\n\tdata.reviews_per_rating = reviews_per_rating\n\tdata.total_reviews = rating_data.total\n\n\treturn data\n\n\ndef set_reviews_in_cache(web_item, reviews_dict):\n\tfrappe.cache().hset(\"item_reviews\", web_item, reviews_dict)\n\n\n@frappe.whitelist()\ndef add_item_review(web_item, title, rating, comment=None):\n\t\"\"\"Add an Item Review by a user if non-existent.\"\"\"\n\tif frappe.session.user == \"Guest\":\n\t\t# guest user should not reach here ideally in the case they do via an API, throw error\n\t\tfrappe.throw(_(\"You are not verified to write a review yet.\"), exc=UnverifiedReviewer)\n\n\tif not frappe.db.exists(\"Item Review\", {\"user\": frappe.session.user, \"website_item\": web_item}):\n\t\tdoc = frappe.new_doc(\"Item Review\")\n\t\tdoc.update(\n\t\t\t{\n\t\t\t\t\"user\": frappe.session.user,\n\t\t\t\t\"customer\": get_customer(),\n\t\t\t\t\"website_item\": web_item,\n\t\t\t\t\"item\": frappe.db.get_value(\"Website Item\", web_item, \"item_code\"),\n\t\t\t\t\"review_title\": title,\n\t\t\t\t\"rating\": rating,\n\t\t\t\t\"comment\": comment,\n\t\t\t}\n\t\t)\n\t\tdoc.published_on = datetime.today().strftime(\"%d %B %Y\")\n\t\tdoc.save()\n\n\ndef get_customer(silent=False):\n\t\"\"\"\n\tsilent: Return customer if exists else return nothing. Dont throw error.\n\t\"\"\"\n\tuser = frappe.session.user\n\tcontact_name = get_contact_name(user)\n\tcustomer = None\n\n\tif contact_name:\n\t\tcontact = frappe.get_doc(\"Contact\", contact_name)\n\t\tfor link in contact.links:\n\t\t\tif link.link_doctype == \"Customer\":\n\t\t\t\tcustomer = link.link_name\n\t\t\t\tbreak\n\n\tif customer:\n\t\treturn frappe.db.get_value(\"Customer\", customer)\n\telif silent:\n\t\treturn None\n\telse:\n\t\t# should not reach here unless via an API\n\t\tfrappe.throw(\n\t\t\t_(\"You are not a verified customer yet. Please contact us to proceed.\"), exc=UnverifiedReviewer\n\t\t)\n"
  },
  {
    "path": "webshop/webshop/doctype/item_review/test_item_review.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# See license.txt\nimport unittest\n\nimport frappe\nfrom frappe.core.doctype.user_permission.test_user_permission import create_user\n\nfrom webshop.webshop.doctype.webshop_settings.test_webshop_settings import (\n\tsetup_webshop_settings,\n)\nfrom webshop.webshop.doctype.item_review.item_review import (\n\tUnverifiedReviewer,\n\tadd_item_review,\n\tget_item_reviews,\n)\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\nfrom webshop.webshop.shopping_cart.cart import get_party\nfrom erpnext.stock.doctype.item.test_item import make_item\n\n\nclass TestItemReview(unittest.TestCase):\n\tdef setUp(self):\n\t\titem = make_item(\"Test Mobile Phone\")\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"}):\n\t\t\tmake_website_item(item, save=True)\n\n\t\tfrappe.set_user(\"Administrator\")\n\t\tsetup_webshop_settings({\"enable_reviews\": 1, \"enabled\": 1})\n\t\tfrappe.local.shopping_cart_settings = None\n\n\tdef tearDown(self):\n\t\tfrappe.set_user(\"Administrator\")\n\n\t\twebsite_item_doc = frappe.get_cached_doc(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"})\n\t\treviews = frappe.get_all(\"Item Review\", {\"website_item\": website_item_doc.name})\n\t\tfor review in reviews:\n\t\t\tfrappe.delete_doc(\"Item Review\", review.name)\n\n\t\twebsite_item_doc.delete()\n\t\tsetup_webshop_settings({\"enable_reviews\": 0})\n\n\tdef test_add_and_get_item_reviews_from_customer(self):\n\t\t\"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)\"\n\t\t# create user\n\t\tweb_item = frappe.db.get_value(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"})\n\t\ttest_user = create_user(\"test_reviewer@example.com\", \"Customer\")\n\t\tfrappe.set_user(test_user.name)\n\n\t\t# create customer and contact against user\n\t\tcustomer = get_party()\n\n\t\t# post review on \"Test Mobile Phone\"\n\t\ttry:\n\t\t\tadd_item_review(web_item, \"Great Product\", 4, \"Would recommend this product\")\n\t\t\treview_name = frappe.db.get_value(\"Item Review\", {\"website_item\": web_item})\n\t\texcept Exception:\n\t\t\tself.fail(f\"Error while publishing review for {web_item}\")\n\n\t\treview_data = get_item_reviews(web_item, 0, 10)\n\n\t\tself.assertEqual(len(review_data.reviews), 1)\n\t\tself.assertTrue(review_data.average_rating)\n\t\tself.assertEqual(review_data.reviews_per_rating[0], 100)\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n\t\tfrappe.delete_doc(\"Item Review\", review_name)\n\t\tcustomer.delete()\n\n\tdef test_add_item_review_from_non_customer(self):\n\t\t\"Check if logged in user (who is not a customer yet) is blocked from posting reviews.\"\n\t\tweb_item = frappe.db.get_value(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"})\n\t\ttest_user = create_user(\"test_reviewer@example.com\", \"Customer\")\n\t\tfrappe.set_user(test_user.name)\n\n\t\twith self.assertRaises(UnverifiedReviewer):\n\t\t\tadd_item_review(web_item, \"Great Product\", 3, \"Would recommend this product\")\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n\n\tdef test_add_item_reviews_from_guest_user(self):\n\t\t\"Check if Guest user is blocked from posting reviews.\"\n\t\tweb_item = frappe.db.get_value(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"})\n\t\tfrappe.set_user(\"Guest\")\n\n\t\twith self.assertRaises(UnverifiedReviewer):\n\t\t\tadd_item_review(web_item, \"Great Product\", 3, \"Would recommend this product\")\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n"
  },
  {
    "path": "webshop/webshop/doctype/override_doctype/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/override_doctype/item.py",
    "content": "import frappe\nfrom frappe import _\nfrom frappe.utils import get_link_to_form\nfrom erpnext.stock.doctype.item.item import Item\nfrom webshop.webshop.doctype.override_doctype.item_group import invalidate_cache_for\n\nclass DataValidationError(frappe.ValidationError):\n\tpass\n\nclass WebshopItem(Item):\n\tdef on_update(self):\n\t\tsuper(WebshopItem, self).on_update()\n\t\tinvalidate_cache_for_item(self)\n\t\tsuper(WebshopItem, self).on_update()\n\n\tdef before_rename(self, old_name, new_name, merge=False):\n\t\tself.validate_duplicate_website_item_before_merge(old_name, new_name)\n\t\treturn super(WebshopItem, self).before_rename(old_name, new_name, merge)\n\n\tdef validate_duplicate_website_item_before_merge(self, old_name, new_name):\n\t\t\"\"\"\n\t\tBlock merge if both old and new items have website items against them.\n\t\tThis is to avoid duplicate website items after merging.\n\t\t\"\"\"\n\t\tweb_items = frappe.get_all(\n\t\t\t\"Website Item\",\n\t\t\tfilters={\"item_code\": [\"in\", [old_name, new_name]]},\n\t\t\tfields=[\"item_code\", \"name\"],\n\t\t)\n\n\t\tif len(web_items) <= 1:\n\t\t\treturn\n\n\t\told_web_item = [d.get(\"name\") for d in web_items if d.get(\"item_code\") == old_name][0]\n\t\tweb_item_link = get_link_to_form(\"Website Item\", old_web_item)\n\t\told_name, new_name = frappe.bold(old_name), frappe.bold(new_name)\n\n\t\tmsg = f\"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}\"\n\t\tfrappe.throw(_(msg), title=_(\"Cannot Merge\"), exc=DataValidationError)\n\n\tdef after_rename(self, old_name, new_name, merge):\n\t\tif self.published_in_website:\n\t\t\tinvalidate_cache_for_item(self)\n\n\t\tsuper(WebshopItem, self).after_rename(old_name, new_name, merge)\n\n\ndef invalidate_cache_for_item(doc):\n\t\"\"\"Invalidate Item Group cache and rebuild ItemVariantsCacheManager.\"\"\"\n\tinvalidate_cache_for(doc, doc.item_group)\n\n\tif doc.get(\"old_item_group\") and doc.get(\"old_item_group\") != doc.item_group:\n\t\tinvalidate_cache_for(doc, doc.old_item_group)\n"
  },
  {
    "path": "webshop/webshop/doctype/override_doctype/item_group.py",
    "content": "import frappe\nfrom frappe import _\nfrom urllib.parse import quote\nfrom frappe.utils import get_url, cint\nfrom frappe.website.website_generator import WebsiteGenerator\nfrom erpnext.setup.doctype.item_group.item_group import ItemGroup\nfrom frappe.website.utils import clear_cache\nfrom webshop.webshop.product_data_engine.filters import ProductFiltersBuilder\n\nclass WebshopItemGroup(ItemGroup, WebsiteGenerator):\n\tnsm_parent_field = \"parent_item_group\"\n\twebsite = frappe._dict(\n\t\tcondition_field=\"show_in_website\",\n\t\ttemplate=\"templates/generators/item_group.html\",\n\t\tno_cache=1,\n\t\tno_breadcrumbs=1,\n\t)\n\n\tdef validate(self):\n\t\tself.make_route()\n\t\tWebsiteGenerator.validate(self)\n\t\tsuper(WebshopItemGroup, self).validate()\n\n\tdef on_update(self):\n\t\tinvalidate_cache_for(self)\n\t\tsuper(WebshopItemGroup, self).on_update()\n\n\tdef make_route(self):\n\t\t\"\"\"Make website route\"\"\"\n\t\tif self.route:\n\t\t\treturn\n\n\t\tself.route = \"\"\n\t\tif self.parent_item_group:\n\t\t\tparent_item_group = frappe.get_doc(\"Item Group\", self.parent_item_group)\n\n\t\t\t# make parent route only if not root\n\t\t\tif parent_item_group.parent_item_group and parent_item_group.route:\n\t\t\t\tself.route = parent_item_group.route + \"/\"\n\n\t\tself.route += self.scrub(self.item_group_name)\n\n\t\treturn self.route\n\n\tdef on_trash(self):\n\t\tWebsiteGenerator.on_trash(self)\n\t\tsuper(WebshopItemGroup, self).on_trash()\n\n\tdef get_context(self, context):\n\t\tcontext.show_search = True\n\t\tcontext.body_class = \"product-page\"\n\t\tcontext.page_length = (\n\t\t\tcint(frappe.db.get_single_value(\"Webshop Settings\", \"products_per_page\")) or 6\n\t\t)\n\t\tcontext.search_link = \"/product_search\"\n\n\t\tfilter_engine = ProductFiltersBuilder(self.name)\n\n\t\tcontext.field_filters = filter_engine.get_field_filters()\n\t\tcontext.attribute_filters = filter_engine.get_attribute_filters()\n\n\t\tcontext.update({\"parents\": get_parent_item_groups(self.parent_item_group), \"title\": self.name})\n\n\t\tif self.slideshow:\n\t\t\tvalues = {\"show_indicators\": 1, \"show_controls\": 0, \"rounded\": 1, \"slider_name\": self.slideshow}\n\t\t\tslideshow = frappe.get_doc(\"Website Slideshow\", self.slideshow)\n\t\t\tslides = slideshow.get({\"doctype\": \"Website Slideshow Item\"})\n\t\t\tfor index, slide in enumerate(slides):\n\t\t\t\tvalues[f\"slide_{index + 1}_image\"] = slide.image\n\t\t\t\tvalues[f\"slide_{index + 1}_title\"] = slide.heading\n\t\t\t\tvalues[f\"slide_{index + 1}_subtitle\"] = slide.description\n\t\t\t\tvalues[f\"slide_{index + 1}_theme\"] = slide.get(\"theme\") or \"Light\"\n\t\t\t\tvalues[f\"slide_{index + 1}_content_align\"] = slide.get(\"content_align\") or \"Centre\"\n\t\t\t\tvalues[f\"slide_{index + 1}_primary_action\"] = slide.url\n\n\t\t\tcontext.slideshow = values\n\n\t\tcontext.no_breadcrumbs = False\n\t\tcontext.title = self.website_title or self.name\n\t\tcontext.name = self.name\n\t\tcontext.item_group_name = self.item_group_name\n\n\t\treturn context\n\n\tdef has_website_permission(self, ptype, user, verbose=False):\n\t\treturn ptype == \"read\"\n\ndef get_item_for_list_in_html(context):\n\t# add missing absolute link in files\n\t# user may forget it during upload\n\tif (context.get(\"website_image\") or \"\").startswith(\"files/\"):\n\t\tcontext[\"website_image\"] = \"/\" + quote(context[\"website_image\"])\n\n\tproducts_template = \"templates/includes/products_as_list.html\"\n\n\treturn frappe.get_template(products_template).render(context)\n\n\ndef get_parent_item_groups(item_group_name, from_item=False):\n\tsettings = frappe.get_cached_doc(\"Webshop Settings\")\n\n\tif settings.enable_field_filters:\n\t\tbase_nav_page = {\"name\": _(\"Shop by Category\"), \"route\": \"/shop-by-category\"}\n\telse:\n\t\tbase_nav_page = {\"name\": _(\"All Products\"), \"route\": \"/all-products\"}\n\n\tif from_item and frappe.request.environ.get(\"HTTP_REFERER\"):\n\t\t# base page after 'Home' will vary on Item page\n\t\tlast_page = frappe.request.environ[\"HTTP_REFERER\"].split(\"/\")[-1].split(\"?\")[0]\n\t\tif last_page and last_page in (\"shop-by-category\", \"all-products\"):\n\t\t\tbase_nav_page_title = \" \".join(last_page.split(\"-\")).title()\n\t\t\tbase_nav_page = {\"name\": _(base_nav_page_title), \"route\": \"/\" + last_page}\n\n\tbase_parents = [\n\t\t{\"name\": _(\"Home\"), \"route\": \"/\"},\n\t\tbase_nav_page,\n\t]\n\n\tif not item_group_name:\n\t\treturn base_parents\n\n\titem_group = frappe.db.get_value(\"Item Group\", item_group_name, [\"lft\", \"rgt\"], as_dict=1)\n\tparent_groups = frappe.db.sql(\n\t\t\"\"\"select name, route from `tabItem Group`\n\t\twhere lft <= %s and rgt >= %s\n\t\tand show_in_website=1\n\t\torder by lft asc\"\"\",\n\t\t(item_group.lft, item_group.rgt),\n\t\tas_dict=True,\n\t)\n\n\treturn base_parents + parent_groups\n\n\ndef invalidate_cache_for(doc, item_group=None):\n\tif not item_group:\n\t\titem_group = doc.name\n\n\tfor d in get_parent_item_groups(item_group):\n\t\titem_group_name = frappe.db.get_value(\"Item Group\", d.get(\"name\"))\n\t\tif item_group_name:\n\t\t\tclear_cache(frappe.db.get_value(\"Item Group\", item_group_name, \"route\"))\n\ndef get_child_groups_for_website(item_group_name, immediate=False, include_self=False):\n\t\"\"\"Returns child item groups *excluding* passed group.\"\"\"\n\titem_group = frappe.get_cached_value(\"Item Group\", item_group_name, [\"lft\", \"rgt\"], as_dict=1)\n\tfilters = {\"lft\": [\">\", item_group.lft], \"rgt\": [\"<\", item_group.rgt], \"show_in_website\": 1}\n\n\tif immediate:\n\t\tfilters[\"parent_item_group\"] = item_group_name\n\n\tif include_self:\n\t\tfilters.update({\"lft\": [\">=\", item_group.lft], \"rgt\": [\"<=\", item_group.rgt]})\n\n\treturn frappe.get_all(\"Item Group\", filters=filters, fields=[\"name\", \"route\"], order_by=\"name\")"
  },
  {
    "path": "webshop/webshop/doctype/override_doctype/payment_request.py",
    "content": "import frappe\nfrom frappe.utils import get_url\n\nfrom erpnext.accounts.doctype.payment_request.payment_request import (\n    PaymentRequest as OriginalPaymentRequest,\n)\n\n\nclass PaymentRequest(OriginalPaymentRequest):\n    def on_payment_authorized(self, status=None):\n        if not status:\n            return\n\n        if status not in (\"Authorized\", \"Completed\"):\n            return\n\n        if not hasattr(frappe.local, \"session\"):\n            return\n\n        if frappe.local.session.user == \"Guest\":\n            return\n\n        cart_settings = frappe.get_doc(\"Webshop Settings\")\n\n        if not cart_settings.enabled:\n            return\n\n        success_url = cart_settings.payment_success_url\n        redirect_to = get_url(\"/orders/{0}\".format(self.reference_name))\n\n        if success_url:\n            redirect_to = (\n                {\n                    \"Orders\": \"/orders\",\n                    \"Invoices\": \"/invoices\",\n                    \"My Account\": \"/me\",\n                }\n            ).get(success_url, \"/me\")\n\n        self.set_as_paid()\n\n        return redirect_to\n\n    @staticmethod\n    def get_gateway_details(args):\n        if args.order_type != \"Shopping Cart\":\n            return super().get_gateway_details(args)\n\n        cart_settings = frappe.get_doc(\"Webshop Settings\")\n        gateway_account = cart_settings.payment_gateway_account\n        return super().get_payment_gateway_account(gateway_account)\n"
  },
  {
    "path": "webshop/webshop/doctype/recommended_items/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/recommended_items/recommended_items.json",
    "content": "{\n \"actions\": [],\n \"creation\": \"2021-07-12 20:52:12.503470\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"website_item\",\n  \"website_item_name\",\n  \"column_break_2\",\n  \"item_code\",\n  \"more_information_section\",\n  \"route\",\n  \"column_break_6\",\n  \"website_item_image\",\n  \"website_item_thumbnail\"\n ],\n \"fields\": [\n  {\n   \"fieldname\": \"website_item\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\"\n  },\n  {\n   \"fetch_from\": \"website_item.web_item_name\",\n   \"fieldname\": \"website_item_name\",\n   \"fieldtype\": \"Data\",\n   \"in_list_view\": 1,\n   \"label\": \"Website Item Name\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"column_break_2\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fieldname\": \"more_information_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"More Information\"\n  },\n  {\n   \"fetch_from\": \"website_item.route\",\n   \"fieldname\": \"route\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Route\",\n   \"read_only\": 1\n  },\n  {\n   \"fetch_from\": \"website_item.website_image\",\n   \"fieldname\": \"website_item_image\",\n   \"fieldtype\": \"Attach\",\n   \"label\": \"Website Item Image\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"column_break_6\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"website_item.thumbnail\",\n   \"fieldname\": \"website_item_thumbnail\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Website Item Thumbnail\",\n   \"read_only\": 1\n  },\n  {\n   \"fetch_from\": \"website_item.item_code\",\n   \"fieldname\": \"item_code\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Item Code\"\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"istable\": 1,\n \"links\": [],\n \"modified\": \"2022-06-28 16:44:24.718728\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Recommended Items\",\n \"owner\": \"Administrator\",\n \"permissions\": [],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"states\": [],\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/recommended_items/recommended_items.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\n# import frappe\nfrom frappe.model.document import Document\n\n\nclass RecommendedItems(Document):\n\tpass\n"
  },
  {
    "path": "webshop/webshop/doctype/webshop_settings/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/webshop_settings/test_webshop_settings.py",
    "content": "# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors\n# See license.txt\nimport unittest\n\nimport frappe\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n\tShoppingCartSetupError,\n)\n\n\nclass TestWebshopSettings(unittest.TestCase):\n\tdef tearDown(self):\n\t\tfrappe.db.rollback()\n\n\tdef test_tax_rule_validation(self):\n\t\tfrappe.db.sql(\"update `tabTax Rule` set use_for_shopping_cart = 0\")\n\t\tfrappe.db.commit()  # nosemgrep\n\n\t\tcart_settings = frappe.get_doc(\"Webshop Settings\")\n\t\tcart_settings.enabled = 1\n\t\tif not frappe.db.get_value(\"Tax Rule\", {\"use_for_shopping_cart\": 1}, \"name\"):\n\t\t\tself.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)\n\n\t\tfrappe.db.sql(\"update `tabTax Rule` set use_for_shopping_cart = 1\")\n\n\tdef test_invalid_filter_fields(self):\n\t\t\"Check if Item fields are blocked in Webshop Settings filter fields.\"\n\t\tfrom frappe.custom.doctype.custom_field.custom_field import create_custom_field\n\n\t\tsetup_webshop_settings({\"enable_field_filters\": 1})\n\n\t\tcreate_custom_field(\n\t\t\t\"Item\",\n\t\t\tdict(owner=\"Administrator\", fieldname=\"test_data\", label=\"Test\", fieldtype=\"Data\"),\n\t\t)\n\t\tsettings = frappe.get_doc(\"Webshop Settings\")\n\t\tsettings.append(\"filter_fields\", {\"fieldname\": \"test_data\"})\n\n\t\tself.assertRaises(frappe.ValidationError, settings.save)\n\n\ndef setup_webshop_settings(values_dict):\n\t\"Accepts a dict of values that updates Webshop Settings.\"\n\tif not values_dict:\n\t\treturn\n\n\tdoc = frappe.get_doc(\"Webshop Settings\", \"Webshop Settings\")\n\tdoc.update(values_dict)\n\tdoc.save()\n\n\ntest_dependencies = [\"Tax Rule\"]\n"
  },
  {
    "path": "webshop/webshop/doctype/webshop_settings/webshop_settings.js",
    "content": "// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n// For license information, please see license.txt\n\nfrappe.ui.form.on(\"Webshop Settings\", {\n\tonload: function(frm) {\n\t\tif(frm.doc.__onload && frm.doc.__onload.quotation_series) {\n\t\t\tfrm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;\n\t\t\tfrm.refresh_field(\"quotation_series\");\n\t\t}\n\n\t\tfrm.set_query('payment_gateway_account', function() {\n\t\t\treturn { 'filters': { \n\t\t\t\t'payment_channel': ['in', [\"Email\", \"Phone\"]] \n\t\t\t } };\n\t\t});\n\t},\n\trefresh: function(frm) {\n\t\tif (frm.doc.enabled) {\n\t\t\tfrm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html(\n\t\t\t\t`<div>${__(\"Follow these steps to create a landing page for your store\")}:\n\t\t\t\t\t<a href=\"https://docs.erpnext.com/docs/user/manual/en/website/store-landing-page\"\n\t\t\t\t\t\tstyle=\"color: var(--gray-600)\">\n\t\t\t\t\t\tdocs/store-landing-page\n\t\t\t\t\t</a>\n\t\t\t\t</div>`\n\t\t\t);\n\t\t}\n\n\t\tfrappe.model.with_doctype(\"Website Item\", () => {\n\t\t\tconst web_item_meta = frappe.get_meta('Website Item');\n\n\t\t\tconst valid_fields = web_item_meta.fields.filter(df =>\n\t\t\t\t[\"Link\", \"Table MultiSelect\"].includes(df.fieldtype) && !df.hidden\n\t\t\t).map(df =>\n\t\t\t\t({ label: df.label, value: df.fieldname })\n\t\t\t);\n\n\t\t\tfrm.get_field(\"filter_fields\").grid.update_docfield_property(\n\t\t\t\t'fieldname', 'options', valid_fields\n\t\t\t);\n\t\t});\n\t},\n\tenabled: function(frm) {\n\t\tif (frm.doc.enabled === 1) {\n\t\t\tfrm.set_value('enable_variants', 1);\n\t\t}\n\t\telse {\n\t\t\tfrm.set_value('company', '');\n\t\t\tfrm.set_value('price_list', '');\n\t\t\tfrm.set_value('default_customer_group', '');\n\t\t\tfrm.set_value('quotation_series', '');\n\t\t}\n\t}\n});\n"
  },
  {
    "path": "webshop/webshop/doctype/webshop_settings/webshop_settings.json",
    "content": "{\n \"actions\": [],\n \"creation\": \"2021-02-10 17:13:39.139103\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"products_per_page\",\n  \"filter_categories_section\",\n  \"enable_field_filters\",\n  \"filter_fields\",\n  \"enable_attribute_filters\",\n  \"filter_attributes\",\n  \"display_settings_section\",\n  \"hide_variants\",\n  \"enable_variants\",\n  \"show_price\",\n  \"column_break_9\",\n  \"login_required_to_view_products\",\n  \"show_stock_availability\",\n  \"show_quantity_in_website\",\n  \"allow_items_not_in_stock\",\n  \"column_break_13\",\n  \"show_apply_coupon_code_in_website\",\n  \"show_contact_us_button\",\n  \"show_attachments\",\n  \"section_break_18\",\n  \"company\",\n  \"price_list\",\n  \"enabled\",\n  \"store_page_docs\",\n  \"column_break_21\",\n  \"default_customer_group\",\n  \"quotation_series\",\n  \"allow_non_website_items_in_cart_quotation\",\n  \"checkout_settings_section\",\n  \"enable_checkout\",\n  \"show_price_in_quotation\",\n  \"column_break_27\",\n  \"save_quotations_as_draft\",\n  \"payment_gateway_account\",\n  \"payment_success_url\",\n  \"add_ons_section\",\n  \"enable_wishlist\",\n  \"column_break_22\",\n  \"enable_reviews\",\n  \"column_break_23\",\n  \"enable_recommendations\",\n  \"item_search_settings_section\",\n  \"redisearch_warning\",\n  \"search_index_fields\",\n  \"is_redisearch_enabled\",\n  \"is_redisearch_loaded\",\n  \"shop_by_category_section\",\n  \"slideshow\",\n  \"guest_display_settings_section\",\n  \"hide_price_for_guest\",\n  \"redirect_on_action\"\n ],\n \"fields\": [\n  {\n   \"default\": \"6\",\n   \"fieldname\": \"products_per_page\",\n   \"fieldtype\": \"Int\",\n   \"label\": \"Products per Page\"\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"filter_categories_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Filters and Categories\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"hide_variants\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Hide Variants\"\n  },\n  {\n   \"default\": \"0\",\n   \"description\": \"The field filters will also work as categories in the <b>Shop by Category</b> page.\",\n   \"fieldname\": \"enable_field_filters\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Field Filters (Categories)\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enable_attribute_filters\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Attribute Filters\"\n  },\n  {\n   \"depends_on\": \"enable_field_filters\",\n   \"fieldname\": \"filter_fields\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Website Item Fields\",\n   \"options\": \"Website Filter Field\"\n  },\n  {\n   \"depends_on\": \"enable_attribute_filters\",\n   \"fieldname\": \"filter_attributes\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Attributes\",\n   \"options\": \"Website Attribute\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enabled\",\n   \"fieldtype\": \"Check\",\n   \"in_list_view\": 1,\n   \"label\": \"Enable Shopping Cart\"\n  },\n  {\n   \"depends_on\": \"doc.enabled\",\n   \"fieldname\": \"store_page_docs\",\n   \"fieldtype\": \"HTML\"\n  },\n  {\n   \"fieldname\": \"display_settings_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Display Settings\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"show_attachments\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Public Attachments\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"show_price\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Price\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"show_stock_availability\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Stock Availability\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enable_variants\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Variant Selection\"\n  },\n  {\n   \"fieldname\": \"column_break_13\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"show_contact_us_button\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Contact Us Button\"\n  },\n  {\n   \"default\": \"0\",\n   \"depends_on\": \"show_stock_availability\",\n   \"fieldname\": \"show_quantity_in_website\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Stock Quantity\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"show_apply_coupon_code_in_website\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Apply Coupon Code\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"allow_items_not_in_stock\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Allow items not in stock to be added to cart\"\n  },\n  {\n   \"fieldname\": \"section_break_18\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Shopping Cart\"\n  },\n  {\n   \"depends_on\": \"enabled\",\n   \"fieldname\": \"company\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Company\",\n   \"mandatory_depends_on\": \"eval: doc.enabled === 1\",\n   \"options\": \"Company\",\n   \"remember_last_selected_value\": 1\n  },\n  {\n   \"depends_on\": \"enabled\",\n   \"description\": \"Prices will not be shown if Price List is not set\",\n   \"fieldname\": \"price_list\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Price List\",\n   \"mandatory_depends_on\": \"eval: doc.enabled === 1\",\n   \"options\": \"Price List\"\n  },\n  {\n   \"fieldname\": \"column_break_21\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"depends_on\": \"enabled\",\n   \"fieldname\": \"default_customer_group\",\n   \"fieldtype\": \"Link\",\n   \"ignore_user_permissions\": 1,\n   \"label\": \"Default Customer Group\",\n   \"mandatory_depends_on\": \"eval: doc.enabled === 1\",\n   \"options\": \"Customer Group\"\n  },\n  {\n   \"depends_on\": \"enabled\",\n   \"fieldname\": \"quotation_series\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Quotation Series\",\n   \"mandatory_depends_on\": \"eval: doc.enabled === 1\"\n  },\n  {\n   \"collapsible\": 1,\n   \"collapsible_depends_on\": \"eval:doc.enable_checkout\",\n   \"depends_on\": \"enabled\",\n   \"fieldname\": \"checkout_settings_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Checkout Settings\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enable_checkout\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Checkout\"\n  },\n  {\n   \"default\": \"Orders\",\n   \"depends_on\": \"enable_checkout\",\n   \"description\": \"After payment completion redirect user to selected page.\",\n   \"fieldname\": \"payment_success_url\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Payment Success Url\",\n   \"mandatory_depends_on\": \"enable_checkout\",\n   \"options\": \"\\nOrders\\nInvoices\\nMy Account\"\n  },\n  {\n   \"fieldname\": \"column_break_27\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"default\": \"0\",\n   \"depends_on\": \"eval: doc.enable_checkout == 0\",\n   \"fieldname\": \"save_quotations_as_draft\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Save Quotations as Draft\"\n  },\n  {\n   \"depends_on\": \"enable_checkout\",\n   \"fieldname\": \"payment_gateway_account\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Payment Gateway Account\",\n   \"mandatory_depends_on\": \"enable_checkout\",\n   \"options\": \"Payment Gateway Account\"\n  },\n  {\n   \"collapsible\": 1,\n   \"depends_on\": \"enable_field_filters\",\n   \"fieldname\": \"shop_by_category_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Shop by Category\"\n  },\n  {\n   \"fieldname\": \"slideshow\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Slideshow\",\n   \"options\": \"Website Slideshow\"\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"add_ons_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Add-ons\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enable_wishlist\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Wishlist\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enable_reviews\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Reviews and Ratings\"\n  },\n  {\n   \"fieldname\": \"search_index_fields\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Search Index Fields\",\n   \"mandatory_depends_on\": \"is_redisearch_enabled\",\n   \"read_only_depends_on\": \"eval:!doc.is_redisearch_loaded\"\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"item_search_settings_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Item Search Settings\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"is_redisearch_loaded\",\n   \"fieldtype\": \"Check\",\n   \"hidden\": 1,\n   \"label\": \"Is Redisearch Loaded\"\n  },\n  {\n   \"depends_on\": \"eval:!doc.is_redisearch_loaded\",\n   \"fieldname\": \"redisearch_warning\",\n   \"fieldtype\": \"HTML\",\n   \"label\": \"Redisearch Warning\",\n   \"options\": \"<p class=\\\"alert alert-warning\\\">Redisearch is not loaded. If you want to use the advanced product search feature, refer <a class=\\\"alert-link\\\" href=\\\"https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/articles/installing-redisearch\\\" target=\\\"_blank\\\">here</a>.</p>\"\n  },\n  {\n   \"default\": \"0\",\n   \"depends_on\": \"eval:doc.show_price\",\n   \"fieldname\": \"hide_price_for_guest\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Hide Price for Guest\"\n  },\n  {\n   \"fieldname\": \"column_break_9\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"guest_display_settings_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Guest Display Settings\"\n  },\n  {\n   \"description\": \"Link to redirect Guest on actions that need login such as add to cart, wishlist, etc. <b>E.g.: /login</b>\",\n   \"fieldname\": \"redirect_on_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Redirect on Action\"\n  },\n  {\n   \"fieldname\": \"column_break_22\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fieldname\": \"column_break_23\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"enable_recommendations\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Recommendations\"\n  },\n  {\n   \"default\": \"0\",\n   \"depends_on\": \"eval: doc.enable_checkout == 0\",\n   \"fieldname\": \"show_price_in_quotation\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Price in Quotation\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"is_redisearch_enabled\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Enable Redisearch\",\n   \"read_only_depends_on\": \"eval:!doc.is_redisearch_loaded\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"login_required_to_view_products\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Login Required to View Products\"\n  },\n  {\n   \"default\": \"0\",\n   \"depends_on\": \"enabled\",\n   \"fieldname\": \"allow_non_website_items_in_cart_quotation\",\n   \"fieldtype\": \"Check\",\n   \"in_list_view\": 1,\n   \"label\": \"Allow Non Website Items in Cart Quotation\"\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"issingle\": 1,\n \"links\": [],\n \"modified\": \"2025-03-22 13:01:51.906319\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Webshop Settings\",\n \"owner\": \"Administrator\",\n \"permissions\": [\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"role\": \"System Manager\",\n   \"share\": 1,\n   \"write\": 1\n  },\n  {\n   \"read\": 1,\n   \"role\": \"All\"\n  }\n ],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"states\": [],\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/webshop_settings/webshop_settings.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport frappe\nfrom frappe import _\nfrom frappe.model.document import Document\nfrom frappe.utils import comma_and, flt, unique\n\nfrom webshop.webshop.redisearch_utils import (\n\tcreate_website_items_index,\n\tdefine_autocomplete_dictionary,\n\tget_indexable_web_fields,\n\tis_search_module_loaded,\n)\n\n\nclass ShoppingCartSetupError(frappe.ValidationError):\n\tpass\n\n\nclass WebshopSettings(Document):\n\tdef onload(self):\n\t\tself.get(\"__onload\").quotation_series = frappe.get_meta(\"Quotation\").get_options(\"naming_series\")\n\n\t\t# flag >> if redisearch is installed and loaded\n\t\tself.is_redisearch_loaded = is_search_module_loaded()\n\n\tdef validate(self):\n\t\tself.validate_field_filters(self.filter_fields, self.enable_field_filters)\n\t\tself.validate_attribute_filters()\n\t\tself.validate_checkout()\n\t\tself.validate_search_index_fields()\n\n\t\tif self.enabled:\n\t\t\tself.validate_price_list_exchange_rate()\n\n\t\tfrappe.clear_document_cache(\"Webshop Settings\", \"Webshop Settings\")\n\n\t\tself.is_redisearch_enabled_pre_save = frappe.db.get_single_value(\n\t\t\t\"Webshop Settings\", \"is_redisearch_enabled\"\n\t\t)\n\n\tdef after_save(self):\n\t\tself.create_redisearch_indexes()\n\n\tdef create_redisearch_indexes(self):\n\t\t# if redisearch is enabled (value changed) create indexes and dictionary\n\t\tvalue_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save\n\t\tif self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed:\n\t\t\tdefine_autocomplete_dictionary()\n\t\t\tcreate_website_items_index()\n\n\t@staticmethod\n\tdef validate_field_filters(filter_fields, enable_field_filters):\n\t\tif not (enable_field_filters and filter_fields):\n\t\t\treturn\n\n\t\tweb_item_meta = frappe.get_meta(\"Website Item\")\n\t\tvalid_fields = [\n\t\t\tdf.fieldname for df in web_item_meta.fields if df.fieldtype in [\"Link\", \"Table MultiSelect\"]\n\t\t]\n\n\t\tfor row in filter_fields:\n\t\t\tif row.fieldname not in valid_fields:\n\t\t\t\tfrappe.throw(\n\t\t\t\t\t_(\n\t\t\t\t\t\t\"Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'\"\n\t\t\t\t\t).format(row.idx, frappe.bold(row.fieldname))\n\t\t\t\t)\n\n\tdef validate_attribute_filters(self):\n\t\tif not (self.enable_attribute_filters and self.filter_attributes):\n\t\t\treturn\n\n\t\t# if attribute filters are enabled, hide_variants should be disabled\n\t\tself.hide_variants = 0\n\n\tdef validate_checkout(self):\n\t\tif self.enable_checkout and not self.payment_gateway_account:\n\t\t\tself.enable_checkout = 0\n\n\tdef validate_search_index_fields(self):\n\t\tif not self.search_index_fields:\n\t\t\treturn\n\n\t\tfields = self.search_index_fields.replace(\" \", \"\")\n\t\tfields = unique(fields.strip(\",\").split(\",\"))  # Remove extra ',' and remove duplicates\n\n\t\t# All fields should be indexable\n\t\tallowed_indexable_fields = get_indexable_web_fields()\n\n\t\tif not (set(fields).issubset(allowed_indexable_fields)):\n\t\t\tinvalid_fields = list(set(fields).difference(allowed_indexable_fields))\n\t\t\tnum_invalid_fields = len(invalid_fields)\n\t\t\tinvalid_fields = comma_and(invalid_fields)\n\n\t\t\tif num_invalid_fields > 1:\n\t\t\t\tfrappe.throw(\n\t\t\t\t\t_(\"{0} are not valid options for Search Index Field.\").format(frappe.bold(invalid_fields))\n\t\t\t\t)\n\t\t\telse:\n\t\t\t\tfrappe.throw(\n\t\t\t\t\t_(\"{0} is not a valid option for Search Index Field.\").format(frappe.bold(invalid_fields))\n\t\t\t\t)\n\n\t\tself.search_index_fields = \",\".join(fields)\n\n\tdef validate_price_list_exchange_rate(self):\n\t\t\"Check if exchange rate exists for Price List currency (to Company's currency).\"\n\t\tfrom erpnext.setup.utils import get_exchange_rate\n\n\t\tif not self.enabled or not self.company or not self.price_list:\n\t\t\treturn  # this function is also called from hooks, check values again\n\n\t\tcompany_currency = frappe.get_cached_value(\"Company\", self.company, \"default_currency\")\n\t\tprice_list_currency = frappe.db.get_value(\"Price List\", self.price_list, \"currency\")\n\n\t\tif not company_currency:\n\t\t\tmsg = f\"Please specify currency in Company {self.company}\"\n\t\t\tfrappe.throw(_(msg), title=_(\"Missing Currency\"), exc=ShoppingCartSetupError)\n\n\t\tif not price_list_currency:\n\t\t\tmsg = f\"Please specify currency in Price List {frappe.bold(self.price_list)}\"\n\t\t\tfrappe.throw(_(msg), title=_(\"Missing Currency\"), exc=ShoppingCartSetupError)\n\n\t\tif price_list_currency != company_currency:\n\t\t\tfrom_currency, to_currency = price_list_currency, company_currency\n\n\t\t\t# Get exchange rate checks Currency Exchange Records too\n\t\t\texchange_rate = get_exchange_rate(from_currency, to_currency, args=\"for_selling\")\n\n\t\t\tif not flt(exchange_rate):\n\t\t\t\tmsg = f\"Missing Currency Exchange Rates for {from_currency}-{to_currency}\"\n\t\t\t\tfrappe.throw(_(msg), title=_(\"Missing\"), exc=ShoppingCartSetupError)\n\n\tdef validate_tax_rule(self):\n\t\tif not frappe.db.get_value(\"Tax Rule\", {\"use_for_shopping_cart\": 1}, \"name\"):\n\t\t\tfrappe.throw(frappe._(\"Set Tax Rule for shopping cart\"), ShoppingCartSetupError)\n\n\tdef get_tax_master(self, billing_territory):\n\t\ttax_master = self.get_name_from_territory(\n\t\t\tbilling_territory, \"sales_taxes_and_charges_masters\", \"sales_taxes_and_charges_master\"\n\t\t)\n\t\treturn tax_master and tax_master[0] or None\n\n\tdef get_shipping_rules(self, shipping_territory):\n\t\treturn self.get_name_from_territory(shipping_territory, \"shipping_rules\", \"shipping_rule\")\n\n\tdef on_change(self):\n\t\told_doc = self.get_doc_before_save()\n\n\t\tif old_doc:\n\t\t\told_fields = old_doc.search_index_fields\n\t\t\tnew_fields = self.search_index_fields\n\n\t\t\t# if search index fields get changed\n\t\t\tif not (new_fields == old_fields):\n\t\t\t\tcreate_website_items_index()\n\n\ndef validate_cart_settings(doc=None, method=None):\n\tfrappe.get_doc(\"Webshop Settings\", \"Webshop Settings\").run_method(\"validate\")\n\n\ndef get_shopping_cart_settings():\n\treturn frappe.get_cached_doc(\"Webshop Settings\")\n\n\n@frappe.whitelist(allow_guest=True)\ndef is_cart_enabled():\n\treturn get_shopping_cart_settings().enabled\n\n\ndef show_quantity_in_website():\n\treturn get_shopping_cart_settings().show_quantity_in_website\n\n\ndef check_shopping_cart_enabled():\n\tif not get_shopping_cart_settings().enabled:\n\t\tfrappe.throw(_(\"You need to enable Shopping Cart\"), ShoppingCartSetupError)\n\n\ndef show_attachments():\n\treturn get_shopping_cart_settings().show_attachments\n"
  },
  {
    "path": "webshop/webshop/doctype/website_item/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/website_item/templates/website_item.html",
    "content": "{% extends \"templates/web.html\" %}\n\n{% block page_content %}\n<h1>{{ title }}</h1>\n{% endblock %}\n\n<!-- this is a sample default web page template -->"
  },
  {
    "path": "webshop/webshop/doctype/website_item/templates/website_item_row.html",
    "content": "<div>\n\t<a href=\"{{ doc.route }}\">{{ doc.title or doc.name }}</a>\n</div>\n<!-- this is a sample default list template -->\n"
  },
  {
    "path": "webshop/webshop/doctype/website_item/test_website_item.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# See license.txt\n\nimport unittest\n\nimport frappe\n\nfrom erpnext.controllers.item_variant import create_variant\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n\tget_shopping_cart_settings,\n)\nfrom webshop.webshop.doctype.webshop_settings.test_webshop_settings import (\n\tsetup_webshop_settings,\n)\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\nfrom webshop.webshop.shopping_cart.product_info import get_product_info_for_website\nfrom webshop.webshop.doctype.override_doctype.item import DataValidationError\nfrom erpnext.stock.doctype.item.test_item import make_item\nfrom webshop.webshop.doctype.override_doctype.item_group import get_parent_item_groups\n\nWEBITEM_DESK_TESTS = (\"test_website_item_desk_item_sync\", \"test_publish_variant_and_template\")\nWEBITEM_PRICE_TESTS = (\n\t\"test_website_item_price_for_logged_in_user\",\n\t\"test_website_item_price_for_guest_user\",\n)\n\n\nclass TestWebsiteItem(unittest.TestCase):\n\t@classmethod\n\tdef setUpClass(cls):\n\t\tsetup_webshop_settings(\n\t\t\t{\n\t\t\t\t\"company\": \"_Test Company\",\n\t\t\t\t\"enabled\": 1,\n\t\t\t\t\"default_customer_group\": \"_Test Customer Group\",\n\t\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t}\n\t\t)\n\n\t@classmethod\n\tdef tearDownClass(cls):\n\t\tfrappe.db.rollback()\n\n\tdef setUp(self):\n\t\tif self._testMethodName in WEBITEM_DESK_TESTS:\n\t\t\tmake_item(\n\t\t\t\t\"Test Web Item\",\n\t\t\t\t{\n\t\t\t\t\t\"has_variant\": 1,\n\t\t\t\t\t\"variant_based_on\": \"Item Attribute\",\n\t\t\t\t\t\"attributes\": [{\"attribute\": \"Test Size\"}],\n\t\t\t\t},\n\t\t\t)\n\t\telif self._testMethodName in WEBITEM_PRICE_TESTS:\n\t\t\tcreate_user_and_customer_if_not_exists(\n\t\t\t\t\"test_contact_customer@example.com\", \"_Test Contact For _Test Customer\"\n\t\t\t)\n\t\t\tcreate_regular_web_item()\n\t\t\tmake_web_item_price(item_code=\"Test Mobile Phone\")\n\n\t\t\t# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.\n\t\t\t# \t  This is because make_web_pricing_rule creates a pricing rule \"selling\": 1, without specifying \"applicable_for\". Therefor,\n\t\t\t# \t  when testing for logged-in user the test will get the previous pricing rule because \"selling\" is still true.\n\t\t\t#\n\t\t\t#     I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.\n\t\t\tmake_web_pricing_rule(\n\t\t\t\ttitle=\"Test Pricing Rule for Test Mobile Phone\", item_code=\"Test Mobile Phone\", selling=1\n\t\t\t)\n\t\t\tmake_web_pricing_rule(\n\t\t\t\ttitle=\"Test Pricing Rule for Test Mobile Phone (Customer)\",\n\t\t\t\titem_code=\"Test Mobile Phone\",\n\t\t\t\tselling=1,\n\t\t\t\tdiscount_percentage=\"25\",\n\t\t\t\tapplicable_for=\"Customer\",\n\t\t\t\tcustomer=\"_Test Customer\",\n\t\t\t)\n\n\tdef test_index_creation(self):\n\t\t\"Check if index is getting created in db.\"\n\t\tfrom webshop.webshop.doctype.website_item.website_item import on_doctype_update\n\n\t\ton_doctype_update()\n\n\t\tindices = frappe.db.sql(\"show index from `tabWebsite Item`\", as_dict=1)\n\t\texpected_columns = {\"route\", \"item_group\", \"brand\"}\n\t\tfor index in indices:\n\t\t\texpected_columns.discard(index.get(\"Column_name\"))\n\n\t\tif expected_columns:\n\t\t\tself.fail(f\"Expected db index on these columns: {', '.join(expected_columns)}\")\n\n\tdef test_website_item_desk_item_sync(self):\n\t\t\"Check creation/updation/deletion of Website Item and its impact on Item master.\"\n\t\tweb_item = None\n\t\titem = make_item(\"Test Web Item\")  # will return item if exists\n\t\ttry:\n\t\t\tweb_item = make_website_item(item, save=False)\n\t\t\tweb_item.save()\n\t\texcept Exception:\n\t\t\tself.fail(f\"Error while creating website item for {item}\")\n\n\t\t# check if website item was created\n\t\tself.assertTrue(bool(web_item))\n\t\tself.assertTrue(bool(web_item.route))\n\n\t\titem.reload()\n\t\tself.assertEqual(web_item.published, 1)\n\t\tself.assertEqual(item.published_in_website, 1)  # check if item was back updated\n\t\tself.assertEqual(web_item.item_group, item.item_group)\n\n\t\t# check if changing item data changes it in website item\n\t\titem.item_name = \"Test Web Item 1\"\n\t\titem.stock_uom = \"Unit\"\n\t\titem.save()\n\t\tweb_item.reload()\n\t\tself.assertEqual(web_item.item_name, item.item_name)\n\t\tself.assertEqual(web_item.stock_uom, item.stock_uom)\n\n\t\t# check if disabling item unpublished website item\n\t\titem.disabled = 1\n\t\titem.save()\n\t\tweb_item.reload()\n\t\tself.assertEqual(web_item.published, 0)\n\n\t\t# check if website item deletion, unpublishes desk item\n\t\tweb_item.delete()\n\t\titem.reload()\n\t\tself.assertEqual(item.published_in_website, 0)\n\n\t\titem.delete()\n\n\tdef test_publish_variant_and_template(self):\n\t\t\"Check if template is published on publishing variant.\"\n\t\t# template \"Test Web Item\" created on setUp\n\t\tvariant = create_variant(\"Test Web Item\", {\"Test Size\": \"Large\"})\n\t\tvariant.save()\n\n\t\t# check if template is not published\n\t\tself.assertIsNone(frappe.db.exists(\"Website Item\", {\"item_code\": variant.variant_of}))\n\n\t\tvariant_web_item = make_website_item(variant, save=False)\n\t\tvariant_web_item.save()\n\n\t\t# check if template is published\n\t\ttry:\n\t\t\ttemplate_web_item = frappe.get_doc(\"Website Item\", {\"item_code\": variant.variant_of})\n\t\texcept frappe.DoesNotExistError:\n\t\t\tself.fail(f\"Template of {variant.item_code}, {variant.variant_of} not published\")\n\n\t\t# teardown\n\t\tvariant_web_item.delete()\n\t\ttemplate_web_item.delete()\n\t\tvariant.delete()\n\n\tdef test_impact_on_merging_items(self):\n\t\t\"Check if merging items is blocked if old and new items both have website items\"\n\t\tfirst_item = make_item(\"Test First Item\")\n\t\tsecond_item = make_item(\"Test Second Item\")\n\n\t\tfirst_web_item = make_website_item(first_item, save=False)\n\t\tfirst_web_item.save()\n\t\tsecond_web_item = make_website_item(second_item, save=False)\n\t\tsecond_web_item.save()\n\n\t\twith self.assertRaises(DataValidationError):\n\t\t\tfrappe.rename_doc(\"Item\", first_item.name, second_item.name, merge=True)\n\n\t\t# tear down\n\t\tsecond_web_item.delete()\n\t\tfirst_web_item.delete()\n\t\tsecond_item.delete()\n\t\tfirst_item.delete()\n\n\t# Website Item Portal Tests Begin\n\n\tdef test_website_item_breadcrumbs(self):\n\t\t\"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group.\"\n\t\titem_code = \"Test Breadcrumb Item\"\n\t\titem = make_item(\n\t\t\titem_code,\n\t\t\t{\n\t\t\t\t\"item_group\": \"_Test Item Group B - 1\",\n\t\t\t},\n\t\t)\n\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": item_code}):\n\t\t\tweb_item = make_website_item(item, save=False)\n\t\t\tweb_item.save()\n\t\telse:\n\t\t\tweb_item = frappe.get_cached_doc(\"Website Item\", {\"item_code\": item_code})\n\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B - 1\", \"show_in_website\", 1)\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B\", \"show_in_website\", 1)\n\n\t\tbreadcrumbs = get_parent_item_groups(item.item_group)\n\n\t\tsettings = frappe.get_cached_doc(\"Webshop Settings\")\n\t\tif settings.enable_field_filters:\n\t\t\tbase_breadcrumb = \"Shop by Category\"\n\t\telse:\n\t\t\tbase_breadcrumb = \"All Products\"\n\n\t\tself.assertEqual(breadcrumbs[0][\"name\"], \"Home\")\n\t\tself.assertEqual(breadcrumbs[1][\"name\"], base_breadcrumb)\n\t\tself.assertEqual(breadcrumbs[2][\"name\"], \"_Test Item Group B\")  # parent item group\n\t\tself.assertEqual(breadcrumbs[3][\"name\"], \"_Test Item Group B - 1\")\n\n\t\t# tear down\n\t\tweb_item.delete()\n\t\titem.delete()\n\n\tdef test_website_item_price_for_logged_in_user(self):\n\t\t\"Check if price details are fetched correctly while logged in.\"\n\t\titem_code = \"Test Mobile Phone\"\n\n\t\t# show price in webshop settings\n\t\tsetup_webshop_settings({\"show_price\": 1})\n\n\t\t# price and pricing rule added via setUp\n\n\t\t# login as customer with pricing rule\n\t\tfrappe.set_user(\"test_contact_customer@example.com\")\n\n\t\t# check if price and slashed price is fetched correctly\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertTrue(bool(data.product_info[\"price\"]))\n\n\t\tprice_object = data.product_info[\"price\"]\n\t\tself.assertEqual(price_object.get(\"discount_percent\"), 25)\n\t\tself.assertEqual(price_object.get(\"price_list_rate\"), 750)\n\t\tself.assertEqual(price_object.get(\"formatted_mrp\"), \"₹ 1,000.00\")\n\t\tself.assertEqual(price_object.get(\"formatted_price\"), \"₹ 750.00\")\n\t\tself.assertEqual(price_object.get(\"formatted_discount_percent\"), \"25.0%\")\n\n\t\t# switch to admin and disable show price\n\t\tfrappe.set_user(\"Administrator\")\n\t\tsetup_webshop_settings({\"show_price\": 0})\n\n\t\t# price should not be fetched for logged in user.\n\t\tfrappe.set_user(\"test_contact_customer@example.com\")\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertFalse(bool(data.product_info[\"price\"]))\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n\n\tdef test_website_item_price_for_guest_user(self):\n\t\t\"Check if price details are fetched correctly for guest user.\"\n\t\titem_code = \"Test Mobile Phone\"\n\n\t\t# show price for guest user in webshop settings\n\t\tsetup_webshop_settings({\"show_price\": 1, \"hide_price_for_guest\": 0})\n\n\t\t# price and pricing rule added via setUp\n\n\t\t# switch to guest user\n\t\tfrappe.set_user(\"Guest\")\n\n\t\t# price should be fetched\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertTrue(bool(data.product_info[\"price\"]))\n\n\t\tprice_object = data.product_info[\"price\"]\n\t\tself.assertEqual(price_object.get(\"discount_percent\"), 10)\n\t\tself.assertEqual(price_object.get(\"price_list_rate\"), 900)\n\n\t\t# hide price for guest user\n\t\tfrappe.set_user(\"Administrator\")\n\t\tsetup_webshop_settings({\"hide_price_for_guest\": 1})\n\t\tfrappe.set_user(\"Guest\")\n\n\t\t# price should not be fetched\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertFalse(bool(data.product_info[\"price\"]))\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n\n\tdef test_website_item_stock_when_out_of_stock(self):\n\t\t\"\"\"\n\t\tCheck if stock details are fetched correctly for empty inventory when:\n\t\t1) Showing stock availability enabled:\n\t\t        - Warehouse unset\n\t\t        - Warehouse set\n\t\t2) Showing stock availability disabled\n\t\t\"\"\"\n\t\titem_code = \"Test Mobile Phone\"\n\t\tuser = frappe.session.user\n\n\t\tfrappe.set_user(\"Administrator\")\n\n\t\tcreate_regular_web_item()\n\t\tsetup_webshop_settings({\"show_stock_availability\": 1})\n\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\n\t\t# check if stock details are fetched and item not in stock without warehouse set\n\t\tself.assertFalse(bool(data.product_info[\"in_stock\"]))\n\t\tself.assertFalse(bool(data.product_info[\"stock_qty\"]))\n\n\t\t# set warehouse\n\t\tfrappe.db.set_value(\n\t\t\t\"Website Item\", {\"item_code\": item_code}, \"website_warehouse\", \"_Test Warehouse - _TC\"\n\t\t)\n\n\t\t# check if stock details are fetched and item not in stock with warehouse set\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertFalse(bool(data.product_info[\"in_stock\"]))\n\t\tself.assertEqual(data.product_info[\"stock_qty\"], 0)\n\n\t\t# disable show stock availability\n\t\tsetup_webshop_settings({\"show_stock_availability\": 0})\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\n\t\t# check if stock detail attributes are not fetched if stock availability is hidden\n\t\tself.assertIsNone(data.product_info.get(\"in_stock\"))\n\t\tself.assertIsNone(data.product_info.get(\"stock_qty\"))\n\t\tself.assertIsNone(data.product_info.get(\"show_stock_qty\"))\n\n\t\t# tear down\n\t\tfrappe.get_cached_doc(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"}).delete()\n\t\tfrappe.set_user(user)\n\n\tdef test_website_item_stock_when_in_stock(self):\n\t\t\"\"\"\n\t\tCheck if stock details are fetched correctly for available inventory when:\n\t\t1) Showing stock availability enabled:\n\t\t        - Warehouse set\n\t\t        - Warehouse unset\n\t\t2) Showing stock availability disabled\n\t\t\"\"\"\n\t\tfrom erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry\n\n\t\titem_code = \"Test Mobile Phone\"\n\t\tuser = frappe.session.user\n\n\t\tfrappe.set_user(\"Administrator\")\n\t\tcreate_regular_web_item()\n\t\tsetup_webshop_settings({\"show_stock_availability\": 1})\n\t\tfrappe.local.shopping_cart_settings = None\n\n\t\t# set warehouse\n\t\tfrappe.db.set_value(\n\t\t\t\"Website Item\", {\"item_code\": item_code}, \"website_warehouse\", \"_Test Warehouse - _TC\"\n\t\t)\n\n\t\t# stock up item\n\t\tstock_entry = make_stock_entry(\n\t\t\titem_code=item_code, target=\"_Test Warehouse - _TC\", qty=2, rate=100\n\t\t)\n\n\t\t# check if stock details are fetched and item is in stock with warehouse set\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertTrue(bool(data.product_info[\"in_stock\"]))\n\t\tself.assertEqual(data.product_info[\"stock_qty\"], 2)\n\n\t\t# unset warehouse\n\t\tfrappe.db.set_value(\"Website Item\", {\"item_code\": item_code}, \"website_warehouse\", \"\")\n\n\t\t# check if stock details are fetched and item not in stock without warehouse set\n\t\t# (even though it has stock in some warehouse)\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\t\tself.assertFalse(bool(data.product_info[\"in_stock\"]))\n\t\tself.assertFalse(bool(data.product_info[\"stock_qty\"]))\n\n\t\t# disable show stock availability\n\t\tsetup_webshop_settings({\"show_stock_availability\": 0})\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tdata = get_product_info_for_website(item_code, skip_quotation_creation=True)\n\n\t\t# check if stock detail attributes are not fetched if stock availability is hidden\n\t\tself.assertIsNone(data.product_info.get(\"in_stock\"))\n\t\tself.assertIsNone(data.product_info.get(\"stock_qty\"))\n\t\tself.assertIsNone(data.product_info.get(\"show_stock_qty\"))\n\n\t\t# tear down\n\t\tstock_entry.cancel()\n\t\tfrappe.get_cached_doc(\"Website Item\", {\"item_code\": \"Test Mobile Phone\"}).delete()\n\t\tfrappe.set_user(user)\n\n\tdef test_recommended_item(self):\n\t\t\"Check if added recommended items are fetched correctly.\"\n\t\titem_code = \"Test Mobile Phone\"\n\t\tweb_item = create_regular_web_item(item_code)\n\n\t\tsetup_webshop_settings({\"enable_recommendations\": 1, \"show_price\": 1})\n\n\t\t# create recommended web item and price for it\n\t\trecommended_web_item = create_regular_web_item(\"Test Mobile Phone 1\")\n\t\tmake_web_item_price(item_code=\"Test Mobile Phone 1\")\n\n\t\t# add recommended item to first web item\n\t\tweb_item.append(\"recommended_items\", {\"website_item\": recommended_web_item.name})\n\t\tweb_item.save()\n\n\t\tfrappe.local.shopping_cart_settings = None\n\t\twebshop_settings = get_shopping_cart_settings()\n\t\trecommended_items = web_item.get_recommended_items(webshop_settings)\n\n\t\t# test results if show price is enabled\n\t\tself.assertEqual(len(recommended_items), 1)\n\t\trecomm_item = recommended_items[0]\n\t\tself.assertEqual(recomm_item.get(\"website_item_name\"), \"Test Mobile Phone 1\")\n\t\tself.assertTrue(bool(recomm_item.get(\"price_info\")))  # price fetched\n\n\t\tprice_info = recomm_item.get(\"price_info\")\n\t\tself.assertEqual(price_info.get(\"price_list_rate\"), 1000)\n\t\tself.assertEqual(price_info.get(\"formatted_price\"), \"₹ 1,000.00\")\n\n\t\t# test results if show price is disabled\n\t\tsetup_webshop_settings({\"show_price\": 0})\n\n\t\tfrappe.local.shopping_cart_settings = None\n\t\twebshop_settings = get_shopping_cart_settings()\n\t\trecommended_items = web_item.get_recommended_items(webshop_settings)\n\n\t\tself.assertEqual(len(recommended_items), 1)\n\t\tself.assertFalse(bool(recommended_items[0].get(\"price_info\")))  # price not fetched\n\n\t\t# tear down\n\t\tweb_item.delete()\n\t\trecommended_web_item.delete()\n\t\tfrappe.get_cached_doc(\"Item\", \"Test Mobile Phone 1\").delete()\n\n\tdef test_recommended_item_for_guest_user(self):\n\t\t\"Check if added recommended items are fetched correctly for guest user.\"\n\t\titem_code = \"Test Mobile Phone\"\n\t\tweb_item = create_regular_web_item(item_code)\n\n\t\t# price visible to guests\n\t\tsetup_webshop_settings(\n\t\t\t{\"enable_recommendations\": 1, \"show_price\": 1, \"hide_price_for_guest\": 0}\n\t\t)\n\n\t\t# create recommended web item and price for it\n\t\trecommended_web_item = create_regular_web_item(\"Test Mobile Phone 1\")\n\t\tmake_web_item_price(item_code=\"Test Mobile Phone 1\")\n\n\t\t# add recommended item to first web item\n\t\tweb_item.append(\"recommended_items\", {\"website_item\": recommended_web_item.name})\n\t\tweb_item.save()\n\n\t\tfrappe.set_user(\"Guest\")\n\n\t\tfrappe.local.shopping_cart_settings = None\n\t\twebshop_settings = get_shopping_cart_settings()\n\t\trecommended_items = web_item.get_recommended_items(webshop_settings)\n\n\t\t# test results if show price is enabled\n\t\tself.assertEqual(len(recommended_items), 1)\n\t\tself.assertTrue(bool(recommended_items[0].get(\"price_info\")))  # price fetched\n\n\t\t# price hidden from guests\n\t\tfrappe.set_user(\"Administrator\")\n\t\tsetup_webshop_settings({\"hide_price_for_guest\": 1})\n\t\tfrappe.set_user(\"Guest\")\n\n\t\tfrappe.local.shopping_cart_settings = None\n\t\twebshop_settings = get_shopping_cart_settings()\n\t\trecommended_items = web_item.get_recommended_items(webshop_settings)\n\n\t\t# test results if show price is enabled\n\t\tself.assertEqual(len(recommended_items), 1)\n\t\tself.assertFalse(bool(recommended_items[0].get(\"price_info\")))  # price fetched\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n\t\tweb_item.delete()\n\t\trecommended_web_item.delete()\n\t\tfrappe.get_cached_doc(\"Item\", \"Test Mobile Phone 1\").delete()\n\n\ndef create_regular_web_item(item_code=None, item_args=None, web_args=None):\n\t\"Create Regular Item and Website Item.\"\n\titem_code = item_code or \"Test Mobile Phone\"\n\titem = make_item(item_code, properties=item_args)\n\n\tif not frappe.db.exists(\"Website Item\", {\"item_code\": item_code}):\n\t\tweb_item = make_website_item(item, save=False)\n\t\tif web_args:\n\t\t\tweb_item.update(web_args)\n\t\tweb_item.save()\n\telse:\n\t\tweb_item = frappe.get_cached_doc(\"Website Item\", {\"item_code\": item_code})\n\n\treturn web_item\n\n\ndef make_web_item_price(**kwargs):\n\titem_code = kwargs.get(\"item_code\")\n\tif not item_code:\n\t\treturn\n\n\tif not frappe.db.exists(\"Item Price\", {\"item_code\": item_code}):\n\t\titem_price = frappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"Item Price\",\n\t\t\t\t\"item_code\": item_code,\n\t\t\t\t\"price_list\": kwargs.get(\"price_list\") or \"_Test Price List India\",\n\t\t\t\t\"price_list_rate\": kwargs.get(\"price_list_rate\") or 1000,\n\t\t\t}\n\t\t)\n\t\titem_price.insert()\n\telse:\n\t\titem_price = frappe.get_cached_doc(\"Item Price\", {\"item_code\": item_code})\n\n\treturn item_price\n\n\ndef make_web_pricing_rule(**kwargs):\n\ttitle = kwargs.get(\"title\")\n\tif not title:\n\t\treturn\n\n\tif not frappe.db.exists(\"Pricing Rule\", title):\n\t\tpricing_rule = frappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"Pricing Rule\",\n\t\t\t\t\"title\": title,\n\t\t\t\t\"apply_on\": kwargs.get(\"apply_on\") or \"Item Code\",\n\t\t\t\t\"items\": [{\"item_code\": kwargs.get(\"item_code\")}],\n\t\t\t\t\"selling\": kwargs.get(\"selling\") or 0,\n\t\t\t\t\"buying\": kwargs.get(\"buying\") or 0,\n\t\t\t\t\"rate_or_discount\": kwargs.get(\"rate_or_discount\") or \"Discount Percentage\",\n\t\t\t\t\"discount_percentage\": kwargs.get(\"discount_percentage\") or 10,\n\t\t\t\t\"company\": kwargs.get(\"company\") or \"_Test Company\",\n\t\t\t\t\"currency\": kwargs.get(\"currency\") or \"INR\",\n\t\t\t\t\"for_price_list\": kwargs.get(\"price_list\") or \"_Test Price List India\",\n\t\t\t\t\"applicable_for\": kwargs.get(\"applicable_for\") or \"\",\n\t\t\t\t\"customer\": kwargs.get(\"customer\") or \"\",\n\t\t\t}\n\t\t)\n\t\tpricing_rule.insert()\n\telse:\n\t\tpricing_rule = frappe.get_doc(\"Pricing Rule\", {\"title\": title})\n\n\treturn pricing_rule\n\n\ndef create_user_and_customer_if_not_exists(email, first_name=None):\n\tif frappe.db.exists(\"User\", email):\n\t\treturn\n\n\tfrappe.get_doc(\n\t\t{\n\t\t\t\"doctype\": \"User\",\n\t\t\t\"user_type\": \"Website User\",\n\t\t\t\"email\": email,\n\t\t\t\"send_welcome_email\": 0,\n\t\t\t\"first_name\": first_name or email.split(\"@\")[0],\n\t\t}\n\t).insert(ignore_permissions=True)\n\n\tcontact = frappe.get_last_doc(\"Contact\", filters={\"email_id\": email})\n\tlink = contact.append(\"links\", {})\n\tlink.link_doctype = \"Customer\"\n\tlink.link_name = \"_Test Customer\"\n\tlink.link_title = \"_Test Customer\"\n\tcontact.save()\n\n\ntest_dependencies = [\"Price List\", \"Item Price\", \"Customer\", \"Contact\", \"Item\"]\n"
  },
  {
    "path": "webshop/webshop/doctype/website_item/website_item.js",
    "content": "// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n// For license information, please see license.txt\n\nfrappe.ui.form.on('Website Item', {\n\tonload: (frm) => {\n\t\t// should never check Private\n\t\tfrm.fields_dict[\"website_image\"].df.is_private = 0;\n\t},\n\n\trefresh: (frm) => {\n\t\tfrm.add_custom_button(__(\"Prices\"), function() {\n\t\t\tfrappe.set_route(\"List\", \"Item Price\", {\"item_code\": frm.doc.item_code});\n\t\t}, __(\"View\"));\n\n\t\tfrm.add_custom_button(__(\"Stock\"), function() {\n\t\t\tfrappe.route_options = {\n\t\t\t\t\"item_code\": frm.doc.item_code\n\t\t\t};\n\t\t\tfrappe.set_route(\"query-report\", \"Stock Balance\");\n\t\t}, __(\"View\"));\n\n\t\tfrm.add_custom_button(__(\"Webshop Settings\"), function() {\n\t\t\tfrappe.set_route(\"Form\", \"Webshop Settings\");\n\t\t}, __(\"View\"));\n\t},\n\n\tcopy_from_item_group: (frm) => {\n\t\treturn frm.call({\n\t\t\tdoc: frm.doc,\n\t\t\tmethod: \"copy_specification_from_item_group\"\n\t\t});\n\t},\n\n\tset_meta_tags: (frm) => {\n\t\tfrappe.utils.set_meta_tag(frm.doc.route);\n\t}\n});\n"
  },
  {
    "path": "webshop/webshop/doctype/website_item/website_item.json",
    "content": "{\n \"actions\": [],\n \"allow_import\": 1,\n \"autoname\": \"naming_series\",\n \"creation\": \"2021-02-09 21:06:14.441698\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"naming_series\",\n  \"web_item_name\",\n  \"route\",\n  \"has_variants\",\n  \"variant_of\",\n  \"published\",\n  \"column_break_3\",\n  \"item_code\",\n  \"item_name\",\n  \"item_group\",\n  \"stock_uom\",\n  \"column_break_11\",\n  \"description\",\n  \"brand\",\n  \"display_section\",\n  \"website_image\",\n  \"website_image_alt\",\n  \"column_break_13\",\n  \"slideshow\",\n  \"thumbnail\",\n  \"stock_information_section\",\n  \"website_warehouse\",\n  \"column_break_24\",\n  \"on_backorder\",\n  \"section_break_17\",\n  \"short_description\",\n  \"web_long_description\",\n  \"column_break_27\",\n  \"website_specifications\",\n  \"copy_from_item_group\",\n  \"display_additional_information_section\",\n  \"show_tabbed_section\",\n  \"tabs\",\n  \"recommended_items_section\",\n  \"recommended_items\",\n  \"offers_section\",\n  \"offers\",\n  \"section_break_6\",\n  \"ranking\",\n  \"set_meta_tags\",\n  \"column_break_22\",\n  \"website_item_groups\",\n  \"advanced_display_section\",\n  \"website_content\"\n ],\n \"fields\": [\n  {\n   \"description\": \"Website display name\",\n   \"fetch_from\": \"item_code.item_name\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"web_item_name\",\n   \"fieldtype\": \"Data\",\n   \"in_list_view\": 1,\n   \"label\": \"Website Item Name\",\n   \"reqd\": 1\n  },\n  {\n   \"fieldname\": \"column_break_3\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fieldname\": \"item_code\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Code\",\n   \"options\": \"Item\",\n   \"read_only_depends_on\": \"eval:!doc.__islocal\",\n   \"reqd\": 1\n  },\n  {\n   \"fetch_from\": \"item_code.item_name\",\n   \"fieldname\": \"item_name\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Item Name\",\n   \"read_only\": 1\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"section_break_6\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Search and SEO\"\n  },\n  {\n   \"fieldname\": \"route\",\n   \"fieldtype\": \"Small Text\",\n   \"in_list_view\": 1,\n   \"label\": \"Route\",\n   \"no_copy\": 1\n  },\n  {\n   \"description\": \"Items with higher ranking will be shown higher\",\n   \"fieldname\": \"ranking\",\n   \"fieldtype\": \"Int\",\n   \"label\": \"Ranking\"\n  },\n  {\n   \"description\": \"Show a slideshow at the top of the page\",\n   \"fieldname\": \"slideshow\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Slideshow\",\n   \"options\": \"Website Slideshow\"\n  },\n  {\n   \"description\": \"Item Image (if not slideshow)\",\n   \"fieldname\": \"website_image\",\n   \"fieldtype\": \"Attach Image\",\n   \"hidden\": 1,\n   \"in_preview\": 1,\n   \"label\": \"Website Image\",\n   \"print_hide\": 1\n  },\n  {\n   \"description\": \"Image Alternative Text\",\n   \"fieldname\": \"website_image_alt\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Image Description\"\n  },\n  {\n   \"fieldname\": \"thumbnail\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Thumbnail\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"column_break_13\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"description\": \"Show Stock availability based on this warehouse.\",\n   \"fieldname\": \"website_warehouse\",\n   \"fieldtype\": \"Link\",\n   \"ignore_user_permissions\": 1,\n   \"label\": \"Website Warehouse\",\n   \"options\": \"Warehouse\"\n  },\n  {\n   \"description\": \"List this Item in multiple groups on the website.\",\n   \"fieldname\": \"website_item_groups\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Website Item Groups\",\n   \"options\": \"Website Item Group\"\n  },\n  {\n   \"fieldname\": \"set_meta_tags\",\n   \"fieldtype\": \"Button\",\n   \"label\": \"Set Meta Tags\"\n  },\n  {\n   \"fieldname\": \"section_break_17\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Display Information\"\n  },\n  {\n   \"fieldname\": \"copy_from_item_group\",\n   \"fieldtype\": \"Button\",\n   \"label\": \"Copy From Item Group\"\n  },\n  {\n   \"fieldname\": \"website_specifications\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Website Specifications\",\n   \"options\": \"Item Website Specification\"\n  },\n  {\n   \"fieldname\": \"web_long_description\",\n   \"fieldtype\": \"Text Editor\",\n   \"label\": \"Website Description\"\n  },\n  {\n   \"description\": \"You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.\",\n   \"fieldname\": \"website_content\",\n   \"fieldtype\": \"HTML Editor\",\n   \"label\": \"Website Content\"\n  },\n  {\n   \"fetch_from\": \"item_code.item_group\",\n   \"fieldname\": \"item_group\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"read_only\": 1,\n   \"search_index\": 1\n  },\n  {\n   \"default\": \"1\",\n   \"fieldname\": \"published\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Published\"\n  },\n  {\n   \"default\": \"0\",\n   \"depends_on\": \"has_variants\",\n   \"fetch_from\": \"item_code.has_variants\",\n   \"fieldname\": \"has_variants\",\n   \"fieldtype\": \"Check\",\n   \"in_standard_filter\": 1,\n   \"label\": \"Has Variants\",\n   \"no_copy\": 1,\n   \"read_only\": 1\n  },\n  {\n   \"depends_on\": \"variant_of\",\n   \"fetch_from\": \"item_code.variant_of\",\n   \"fieldname\": \"variant_of\",\n   \"fieldtype\": \"Link\",\n   \"ignore_user_permissions\": 1,\n   \"in_standard_filter\": 1,\n   \"label\": \"Variant Of\",\n   \"options\": \"Item\",\n   \"read_only\": 1,\n   \"search_index\": 1,\n   \"set_only_once\": 1\n  },\n  {\n   \"fetch_from\": \"item_code.stock_uom\",\n   \"fieldname\": \"stock_uom\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Stock UOM\",\n   \"options\": \"UOM\",\n   \"read_only\": 1\n  },\n  {\n   \"depends_on\": \"brand\",\n   \"fetch_from\": \"item_code.brand\",\n   \"fieldname\": \"brand\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Brand\",\n   \"options\": \"Brand\",\n   \"search_index\": 1\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"advanced_display_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Advanced Display Content\"\n  },\n  {\n   \"fieldname\": \"display_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Display Images\"\n  },\n  {\n   \"fieldname\": \"column_break_27\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fieldname\": \"column_break_22\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.description\",\n   \"fieldname\": \"description\",\n   \"fieldtype\": \"Text Editor\",\n   \"label\": \"Item Description\",\n   \"read_only\": 1\n  },\n  {\n   \"default\": \"WEB-ITM-.####\",\n   \"fieldname\": \"naming_series\",\n   \"fieldtype\": \"Select\",\n   \"hidden\": 1,\n   \"label\": \"Naming Series\",\n   \"no_copy\": 1,\n   \"options\": \"WEB-ITM-.####\",\n   \"print_hide\": 1\n  },\n  {\n   \"fieldname\": \"display_additional_information_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Display Additional Information\"\n  },\n  {\n   \"depends_on\": \"show_tabbed_section\",\n   \"fieldname\": \"tabs\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Tabs\",\n   \"options\": \"Website Item Tabbed Section\"\n  },\n  {\n   \"default\": \"0\",\n   \"fieldname\": \"show_tabbed_section\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Add Section with Tabs\"\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"offers_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Offers\"\n  },\n  {\n   \"fieldname\": \"offers\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Offers to Display\",\n   \"options\": \"Website Offer\"\n  },\n  {\n   \"fieldname\": \"column_break_11\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"description\": \"Short Description for List View\",\n   \"fieldname\": \"short_description\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Short Website Description\"\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"recommended_items_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Recommended Items\"\n  },\n  {\n   \"fieldname\": \"recommended_items\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Recommended/Similar Items\",\n   \"options\": \"Recommended Items\"\n  },\n  {\n   \"fieldname\": \"stock_information_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Stock Information\"\n  },\n  {\n   \"fieldname\": \"column_break_24\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"default\": \"0\",\n   \"description\": \"Indicate that Item is available on backorder and not usually pre-stocked\",\n   \"fieldname\": \"on_backorder\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"On Backorder\"\n  }\n ],\n \"has_web_view\": 1,\n \"image_field\": \"website_image\",\n \"index_web_pages_for_search\": 1,\n \"links\": [],\n \"make_attachments_public\": 1,\n \"modified\": \"2024-11-05 13:41:45.347700\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Website Item\",\n \"naming_rule\": \"Expression (old style)\",\n \"owner\": \"Administrator\",\n \"permissions\": [\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"System Manager\",\n   \"share\": 1,\n   \"write\": 1\n  },\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"Website Manager\",\n   \"share\": 1,\n   \"write\": 1\n  },\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"Stock User\",\n   \"share\": 1,\n   \"write\": 1\n  },\n  {\n   \"create\": 1,\n   \"delete\": 1,\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"Stock Manager\",\n   \"share\": 1,\n   \"write\": 1\n  }\n ],\n \"search_fields\": \"web_item_name, item_code, item_group\",\n \"show_name_in_global_search\": 1,\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"states\": [],\n \"title_field\": \"web_item_name\",\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/website_item/website_item.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport json\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from erpnext.stock.doctype.item.item import Item\n\nimport frappe\nfrom frappe import _\nfrom frappe.utils import cint, cstr, flt, random_string\nfrom frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow\nfrom frappe.website.website_generator import WebsiteGenerator\n\nfrom webshop.webshop.doctype.item_review.item_review import get_item_reviews\nfrom webshop.webshop.redisearch_utils import (\n    delete_item_from_index,\n    insert_item_to_index,\n    update_index_for_item,\n)\nfrom webshop.webshop.shopping_cart.cart import _set_price_list\nfrom webshop.webshop.doctype.override_doctype.item_group import (\n    get_parent_item_groups,\n    invalidate_cache_for,\n)\nfrom erpnext.stock.doctype.item.item import Item\nfrom erpnext.utilities.product import get_price\nfrom webshop.webshop.shopping_cart.cart import get_party\nfrom webshop.webshop.variant_selector.item_variants_cache import (\n    ItemVariantsCacheManager,\n)\n\n\nclass WebsiteItem(WebsiteGenerator):\n\twebsite = frappe._dict(\n\t\tpage_title_field=\"web_item_name\",\n\t\tcondition_field=\"published\",\n\t\ttemplate=\"templates/generators/item/item.html\",\n\t\tno_cache=1,\n\t)\n\n\tdef autoname(self):\n\t\t# use naming series to accomodate items with same name (different item code)\n\t\tfrom frappe.model.naming import get_default_naming_series, make_autoname\n\n\t\tnaming_series = get_default_naming_series(\"Website Item\")\n\t\tif not self.name and naming_series:\n\t\t\tself.name = make_autoname(naming_series, doc=self)\n\n\tdef onload(self):\n\t\tsuper(WebsiteItem, self).onload()\n\n\tdef validate(self):\n\t\tsuper(WebsiteItem, self).validate()\n\n\t\tif not self.item_code:\n\t\t\tfrappe.throw(_(\"Item Code is required\"), title=_(\"Mandatory\"))\n\n\t\tself.validate_duplicate_website_item()\n\t\tself.validate_website_image()\n\t\tself.make_thumbnail()\n\t\tself.publish_unpublish_desk_item(publish=True)\n\n\t\tif not self.get(\"__islocal\"):\n\t\t\twig = frappe.qb.DocType(\"Website Item Group\")\n\t\t\tquery = (\n\t\t\t\tfrappe.qb.from_(wig)\n\t\t\t\t.select(wig.item_group)\n\t\t\t\t.where(\n\t\t\t\t\t(wig.parentfield == \"website_item_groups\")\n\t\t\t\t\t& (wig.parenttype == \"Website Item\")\n\t\t\t\t\t& (wig.parent == self.name)\n\t\t\t\t)\n\t\t\t)\n\t\t\tresult = query.run(as_list=True)\n\n\t\t\tself.old_website_item_groups = [x[0] for x in result]\n\n\tdef on_update(self):\n\t\tinvalidate_cache_for_web_item(self)\n\t\tself.update_template_item()\n\n\tdef on_trash(self):\n\t\tsuper(WebsiteItem, self).on_trash()\n\t\tdelete_item_from_index(self)\n\t\tself.publish_unpublish_desk_item(publish=False)\n\n\tdef validate_duplicate_website_item(self):\n\t\texisting_web_item = frappe.db.exists(\n\t\t\t\"Website Item\", {\"item_code\": self.item_code}\n\t\t)\n\t\tif existing_web_item and existing_web_item != self.name:\n\t\t\tmessage = _(\"Website Item already exists against Item {0}\").format(\n\t\t\t\tfrappe.bold(self.item_code)\n\t\t\t)\n\t\t\tfrappe.throw(message, title=_(\"Already Published\"))\n\n\tdef publish_unpublish_desk_item(self, publish=True):\n\t\tif (\n\t\t\tfrappe.db.get_value(\"Item\", self.item_code, \"published_in_website\")\n\t\t\tand publish\n\t\t):\n\t\t\treturn  # if already published don't publish again\n\t\tfrappe.db.set_value(\"Item\", self.item_code, \"published_in_website\", publish)\n\n\tdef make_route(self):\n\t\t\"\"\"Called from set_route in WebsiteGenerator.\"\"\"\n\t\tif not self.route:\n\t\t\treturn (\n\t\t\t\tcstr(frappe.db.get_value(\"Item Group\", self.item_group, \"route\"))\n\t\t\t\t+ \"/\"\n\t\t\t\t+ self.scrub(\n\t\t\t\t\t(self.item_name if self.item_name else self.item_code)\n\t\t\t\t\t+ \"-\"\n\t\t\t\t\t+ random_string(5)\n\t\t\t\t)\n\t\t\t)\n\n\tdef update_template_item(self):\n\t\t\"\"\"Publish Template Item if Variant is published.\"\"\"\n\t\tif self.variant_of:\n\t\t\tif self.published:\n\t\t\t\t# show template\n\t\t\t\ttemplate_item = frappe.get_doc(\"Item\", self.variant_of)\n\n\t\t\t\tif not template_item.published_in_website:\n\t\t\t\t\ttemplate_item.flags.ignore_permissions = True\n\t\t\t\t\tmake_website_item(template_item)\n\n\tdef validate_website_image(self):\n\t\tif frappe.flags.in_import:\n\t\t\treturn\n\n\t\t\"\"\"Validate if the website image is a public file\"\"\"\n\t\tif not self.website_image:\n\t\t\treturn\n\n\t\t# find if website image url exists as public\n\t\tfile_doc = frappe.get_all(\n\t\t\t\"File\",\n\t\t\tfilters={\"file_url\": self.website_image},\n\t\t\tfields=[\"name\", \"is_private\"],\n\t\t\torder_by=\"is_private asc\",\n\t\t\tlimit_page_length=1,\n\t\t)\n\n\t\tif file_doc:\n\t\t\tfile_doc = file_doc[0]\n\n\t\tif not file_doc:\n\t\t\tfrappe.msgprint(\n\t\t\t\t_(\"Website Image {0} attached to Item {1} cannot be found\").format(\n\t\t\t\t\tself.website_image, self.name\n\t\t\t\t)\n\t\t\t)\n\n\t\t\tself.website_image = None\n\n\t\telif file_doc.is_private:\n\t\t\tfrappe.msgprint(_(\"Website Image should be a public file or website URL\"))\n\n\t\t\tself.website_image = None\n\n\tdef make_thumbnail(self):\n\t\t\"\"\"Make a thumbnail of `website_image`\"\"\"\n\t\tif frappe.flags.in_import or frappe.flags.in_migrate:\n\t\t\treturn\n\n\t\timport requests.exceptions\n\n\t\tdb_website_image = frappe.db.get_value(self.doctype, self.name, \"website_image\")\n\t\tif not self.is_new() and self.website_image != db_website_image:\n\t\t\tself.thumbnail = None\n\n\t\tif self.website_image and not self.thumbnail:\n\t\t\tfile_doc = None\n\n\t\t\ttry:\n\t\t\t\tfile_doc = frappe.get_doc(\n\t\t\t\t\t\"File\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"file_url\": self.website_image,\n\t\t\t\t\t\t\"attached_to_doctype\": \"Website Item\",\n\t\t\t\t\t\t\"attached_to_name\": self.name,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\texcept frappe.DoesNotExistError:\n\t\t\t\tpass\n\t\t\t\t# cleanup\n\t\t\t\tfrappe.local.message_log.pop()\n\n\t\t\texcept requests.exceptions.HTTPError:\n\t\t\t\tfrappe.msgprint(\n\t\t\t\t\t_(\"Warning: Invalid attachment {0}\").format(self.website_image)\n\t\t\t\t)\n\t\t\t\tself.website_image = None\n\n\t\t\texcept requests.exceptions.SSLError:\n\t\t\t\tfrappe.msgprint(\n\t\t\t\t\t_(\"Warning: Invalid SSL certificate on attachment {0}\").format(\n\t\t\t\t\t\tself.website_image\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t\tself.website_image = None\n\n\t\t\t# for CSV import\n\t\t\tif self.website_image and not file_doc:\n\t\t\t\ttry:\n\t\t\t\t\tfile_doc = frappe.get_doc(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"doctype\": \"File\",\n\t\t\t\t\t\t\t\"file_url\": self.website_image,\n\t\t\t\t\t\t\t\"attached_to_doctype\": \"Website Item\",\n\t\t\t\t\t\t\t\"attached_to_name\": self.name,\n\t\t\t\t\t\t}\n\t\t\t\t\t).save()\n\n\t\t\t\texcept IOError:\n\t\t\t\t\tself.website_image = None\n\n\t\t\tif file_doc:\n\t\t\t\tif not file_doc.thumbnail_url:\n\t\t\t\t\tfile_doc.make_thumbnail()\n\n\t\t\t\tself.thumbnail = file_doc.thumbnail_url\n\n\tdef get_context(self, context):\n\t\tcontext.show_search = True\n\t\tcontext.search_link = \"/search\"\n\t\tcontext.body_class = \"product-page\"\n\n\t\tcontext.parents = get_parent_item_groups(\n\t\t\tself.item_group, from_item=True\n\t\t)  # breadcumbs\n\t\tself.attributes = frappe.get_all(\n\t\t\t\"Item Variant Attribute\",\n\t\t\tfields=[\"attribute\", \"attribute_value\"],\n\t\t\tfilters={\"parent\": self.item_code},\n\t\t)\n\n\t\tif self.slideshow:\n\t\t\tcontext.update(get_slideshow(self))\n\n\t\tself.set_metatags(context)\n\t\tself.set_shopping_cart_data(context)\n\n\t\tsettings = context.shopping_cart.cart_settings\n\n\t\tself.get_product_details_section(context)\n\n\t\tif settings.get(\"enable_reviews\"):\n\t\t\treviews_data = get_item_reviews(self.name)\n\t\t\tcontext.update(reviews_data)\n\t\t\tcontext.reviews = context.reviews[:4]\n\n\t\tcontext.wished = False\n\t\tif frappe.db.exists(\n\t\t\t\"Wishlist Item\",\n\t\t\t{\"item_code\": self.item_code, \"parent\": frappe.session.user},\n\t\t):\n\t\t\tcontext.wished = True\n\n\t\tcontext.user_is_customer = check_if_user_is_customer()\n\n\t\tcontext.recommended_items = None\n\t\tif settings and settings.enable_recommendations:\n\t\t\tcontext.recommended_items = self.get_recommended_items(settings)\n\n\t\treturn context\n\n\tdef set_selected_attributes(self, variants, context, attribute_values_available):\n\t\tfor variant in variants:\n\t\t\tvariant.attributes = frappe.get_all(\n\t\t\t\t\"Item Variant Attribute\",\n\t\t\t\tfilters={\"parent\": variant.name},\n\t\t\t\tfields=[\"attribute\", \"attribute_value as value\"],\n\t\t\t)\n\n\t\t\t# make an attribute-value map for easier access in templates\n\t\t\tvariant.attribute_map = frappe._dict(\n\t\t\t\t{attr.attribute: attr.value for attr in variant.attributes}\n\t\t\t)\n\n\t\t\tfor attr in variant.attributes:\n\t\t\t\tvalues = attribute_values_available.setdefault(attr.attribute, [])\n\t\t\t\tif attr.value not in values:\n\t\t\t\t\tvalues.append(attr.value)\n\n\t\t\t\tif variant.name == context.variant.name:\n\t\t\t\t\tcontext.selected_attributes[attr.attribute] = attr.value\n\n\tdef set_attribute_values(self, attributes, context, attribute_values_available):\n\t\tfor attr in attributes:\n\t\t\tvalues = context.attribute_values.setdefault(attr.attribute, [])\n\n\t\t\tif cint(\n\t\t\t\tfrappe.db.get_value(\"Item Attribute\", attr.attribute, \"numeric_values\")\n\t\t\t):\n\t\t\t\tfor val in sorted(\n\t\t\t\t\tattribute_values_available.get(attr.attribute, []), key=flt\n\t\t\t\t):\n\t\t\t\t\tvalues.append(val)\n\t\t\telse:\n\t\t\t\t# get list of values defined (for sequence)\n\t\t\t\tfor attr_value in frappe.db.get_all(\n\t\t\t\t\t\"Item Attribute Value\",\n\t\t\t\t\tfields=[\"attribute_value\"],\n\t\t\t\t\tfilters={\"parent\": attr.attribute},\n\t\t\t\t\torder_by=\"idx asc\",\n\t\t\t\t):\n\n\t\t\t\t\tif attr_value.attribute_value in attribute_values_available.get(\n\t\t\t\t\t\tattr.attribute, []\n\t\t\t\t\t):\n\t\t\t\t\t\tvalues.append(attr_value.attribute_value)\n\n\tdef set_metatags(self, context):\n\t\tcontext.metatags = frappe._dict({})\n\n\t\tsafe_description = frappe.utils.to_markdown(self.description)\n\n\t\tcontext.metatags.url = frappe.utils.get_url() + \"/\" + context.route\n\n\t\tif context.website_image:\n\t\t\tif context.website_image.startswith(\"http\"):\n\t\t\t\turl = context.website_image\n\t\t\telse:\n\t\t\t\turl = frappe.utils.get_url() + context.website_image\n\t\t\tcontext.metatags.image = url\n\n\t\tcontext.metatags.description = safe_description[:300]\n\n\t\tcontext.metatags.title = self.web_item_name or self.item_name or self.item_code\n\n\t\tcontext.metatags[\"og:type\"] = \"product\"\n\t\tcontext.metatags[\"og:site_name\"] = \"ERPNext\"\n\n\tdef set_shopping_cart_data(self, context):\n\t\tfrom webshop.webshop.shopping_cart.product_info import (\n\t\t\tget_product_info_for_website,\n\t\t)\n\n\t\tcontext.shopping_cart = get_product_info_for_website(\n\t\t\tself.item_code, skip_quotation_creation=True\n\t\t)\n\n\t@frappe.whitelist()\n\tdef copy_specification_from_item_group(self):\n\t\tself.set(\"website_specifications\", [])\n\t\tif self.item_group:\n\t\t\tfor label, desc in frappe.db.get_values(\n\t\t\t\t\"Item Website Specification\",\n\t\t\t\t{\"parent\": self.item_group},\n\t\t\t\t[\"label\", \"description\"],\n\t\t\t):\n\t\t\t\trow = self.append(\"website_specifications\")\n\t\t\t\trow.label = label\n\t\t\t\trow.description = desc\n\n\tdef get_product_details_section(self, context):\n\t\t\"\"\"Get section with tabs or website specifications.\"\"\"\n\t\tcontext.show_tabs = self.show_tabbed_section\n\t\tif self.show_tabbed_section and (self.tabs or self.website_specifications):\n\t\t\tcontext.tabs = self.get_tabs()\n\t\telse:\n\t\t\tcontext.website_specifications = self.website_specifications\n\n\tdef get_tabs(self):\n\t\ttab_values = {}\n\t\ttab_values[\"tab_1_title\"] = _(\"Product Details\")\n\t\ttab_values[\"tab_1_content\"] = frappe.render_template(\n\t\t\t\"templates/generators/item/item_specifications.html\",\n\t\t\t{\n\t\t\t\t\"website_specifications\": self.website_specifications,\n\t\t\t\t\"show_tabs\": self.show_tabbed_section,\n\t\t\t},\n\t\t)\n\n\t\tfor row in self.tabs:\n\t\t\ttab_values[f\"tab_{row.idx + 1}_title\"] = _(row.label)\n\t\t\ttab_values[f\"tab_{row.idx + 1}_content\"] = row.content\n\n\t\treturn tab_values\n\n\tdef get_recommended_items(self, settings):\n\t\tri = frappe.qb.DocType(\"Recommended Items\")\n\t\twi = frappe.qb.DocType(\"Website Item\")\n\n\t\tquery = (\n\t\t\tfrappe.qb.from_(ri)\n\t\t\t.join(wi)\n\t\t\t.on(ri.item_code == wi.item_code)\n\t\t\t.select(\n\t\t\t\tri.item_code, ri.route, ri.website_item_name, ri.website_item_thumbnail\n\t\t\t)\n\t\t\t.where((ri.parent == self.name) & (wi.published == 1))\n\t\t\t.orderby(ri.idx)\n\t\t)\n\t\titems = query.run(as_dict=True)\n\n\t\tif settings.show_price:\n\t\t\tis_guest = frappe.session.user == \"Guest\"\n\t\t\t# Show Price if logged in.\n\t\t\t# If not logged in and price is hidden for guest, skip price fetch.\n\t\t\tif is_guest and settings.hide_price_for_guest:\n\t\t\t\treturn items\n\n\t\t\tselling_price_list = _set_price_list(settings, None)\n\t\t\tparty = get_party()\n\n\t\t\tfor item in items:\n\t\t\t\titem.price_info = get_price(\n\t\t\t\t\titem.item_code,\n\t\t\t\t\tselling_price_list,\n\t\t\t\t\tsettings.default_customer_group,\n\t\t\t\t\tsettings.company,\n\t\t\t\t\tparty=party,\n\t\t\t\t)\n\n\t\treturn items\n\n\ndef invalidate_item_variants_cache_for_website(doc):\n\t\"\"\"\n\tRebuild ItemVariantsCacheManager via Item or Website Item\n\n\tArgs:\n\t\tdoc (Item): item of which cache should be cleared\n\t\"\"\"\n\titem_code = None\n\tis_web_item = doc.get(\"published_in_website\") or doc.get(\"published\")\n\n\tif doc.has_variants and is_web_item:\n\t\titem_code = doc.item_code\n\telif doc.variant_of and frappe.db.get_value(\n\t\t\"Item\", doc.variant_of, \"published_in_website\"\n\t):\n\t\titem_code = doc.variant_of\n\n\tif not item_code:\n\t\treturn\n\n\titem_cache = ItemVariantsCacheManager(item_code)\n\titem_cache.rebuild_cache()\n\n\ndef invalidate_cache_for_web_item(doc):\n\t\"\"\"\n\tInvalidate Website Item Group cache and rebuild ItemVariantsCacheManager\n\tArgs:\n\t\tdoc (Item): document against which cache should be cleared\n\t\"\"\"\n\tinvalidate_cache_for(doc, doc.item_group)\n\n\twebsite_item_groups = list(\n\t\tset(\n\t\t\t(doc.get(\"old_website_item_groups\") or [])\n\t\t\t+ [\n\t\t\t\td.item_group\n\t\t\t\tfor d in doc.get({\"doctype\": \"Website Item Group\"})\n\t\t\t\tif d.item_group\n\t\t\t]\n\t\t)\n\t)\n\n\tfor item_group in website_item_groups:\n\t\tinvalidate_cache_for(doc, item_group)\n\n\t# Update Search Cache\n\tupdate_index_for_item(doc)\n\n\tinvalidate_item_variants_cache_for_website(doc)\n\n\ndef on_doctype_update():\n\t# since route is a Text column, it needs a length for indexing\n\tfrappe.db.add_index(\"Website Item\", [\"route(500)\"])\n\n\ndef check_if_user_is_customer(user=None):\n\tfrom frappe.contacts.doctype.contact.contact import get_contact_name\n\n\tif not user:\n\t\tuser = frappe.session.user\n\n\tcontact_name = get_contact_name(user)\n\tcustomer = None\n\n\tif contact_name:\n\t\tcontact = frappe.get_doc(\"Contact\", contact_name)\n\t\tfor link in contact.links:\n\t\t\tif link.link_doctype == \"Customer\":\n\t\t\t\tcustomer = link.link_name\n\t\t\t\tbreak\n\n\treturn True if customer else False\n\n\n@frappe.whitelist()\ndef make_website_item(doc, save=True):\n\t\"\"\"\n\tMake Website Item from Item. Used via Form UI or patch.\n\t\"\"\"\n\tif not doc:\n\t\treturn\n\n\tif isinstance(doc, str):\n\t\tdoc = json.loads(doc)\n\n\tif frappe.db.exists(\"Website Item\", {\"item_code\": doc.get(\"item_code\")}):\n\t\tmessage = _(\"Website Item already exists against {0}\").format(\n\t\t\tfrappe.bold(doc.get(\"item_code\"))\n\t\t)\n\t\tfrappe.throw(message, title=_(\"Already Published\"))\n\n\twebsite_item = frappe.new_doc(\"Website Item\")\n\twebsite_item.web_item_name = doc.get(\"item_name\")\n\n\tfields_to_map = [\n\t\t\"item_code\",\n\t\t\"item_name\",\n\t\t\"item_group\",\n\t\t\"stock_uom\",\n\t\t\"brand\",\n\t\t\"has_variants\",\n\t\t\"variant_of\",\n\t\t\"description\",\n\t]\n\tfor field in fields_to_map:\n\t\twebsite_item.update({field: doc.get(field)})\n\n\t# Needed for publishing/mapping via Form UI only\n\tif not frappe.flags.in_migrate and (\n\t\tdoc.get(\"image\") and not website_item.website_image\n\t):\n\t\twebsite_item.website_image = doc.get(\"image\")\n\n\tif not save:\n\t\treturn website_item\n\n\twebsite_item.save()\n\n\t# Add to search cache\n\tinsert_item_to_index(website_item)\n\n\treturn [website_item.name, website_item.web_item_name]\n\n@frappe.whitelist()\ndef has_website_permission_for_website_item(doc, ptype, user, verbose=False):\n\t# Check item group permissions for website\n\n\tif user == \"Administrator\":\n\t\treturn True\n\n\tif frappe.has_permission(\"Website Item\", ptype=ptype, doc=doc, user=user):\n\t\treturn True\n\n\tif not frappe.db.get_single_value(\"Webshop Settings\", \"login_required_to_view_products\"):\n\t\treturn True\n\n\treturn False\n\n@frappe.whitelist()\ndef has_website_permission_for_item_group(doc, ptype, user, verbose=False):\n\t# Check item group permissions for website\n\tif user == \"Administrator\":\n\t\treturn True\n\n\tif frappe.has_permission(\"Item Group\", ptype=ptype, doc=doc, user=user):\n\t\treturn True\n\n\tif not frappe.db.get_single_value(\"Webshop Settings\", \"login_required_to_view_products\"):\n\t\treturn True\n\n\treturn False\n"
  },
  {
    "path": "webshop/webshop/doctype/website_item/website_item_list.js",
    "content": "frappe.listview_settings['Website Item'] = {\n\tadd_fields: [\"item_name\", \"web_item_name\", \"published\", \"website_image\", \"has_variants\", \"variant_of\"],\n\tfilters: [[\"published\", \"=\", \"1\"]],\n\n\tget_indicator: function(doc) {\n\t\tif (doc.has_variants && doc.published) {\n\t\t\treturn [__(\"Template\"), \"orange\", \"has_variants,=,Yes|published,=,1\"];\n\t\t} else if (doc.has_variants && !doc.published) {\n\t\t\treturn [__(\"Template\"), \"grey\", \"has_variants,=,Yes|published,=,0\"];\n\t\t} else if (doc.variant_of  && doc.published) {\n\t\t\treturn [__(\"Variant\"), \"blue\", \"published,=,1|variant_of,=,\" + doc.variant_of];\n\t\t} else if (doc.variant_of  && !doc.published) {\n\t\t\treturn [__(\"Variant\"), \"grey\", \"published,=,0|variant_of,=,\" + doc.variant_of];\n\t\t} else if (doc.published) {\n\t\t\treturn [__(\"Published\"), \"green\", \"published,=,1\"];\n\t\t} else {\n\t\t\treturn [__(\"Not Published\"), \"grey\", \"published,=,0\"];\n\t\t}\n\t}\n};"
  },
  {
    "path": "webshop/webshop/doctype/website_item_tabbed_section/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/website_item_tabbed_section/website_item_tabbed_section.json",
    "content": "{\n \"actions\": [],\n \"creation\": \"2021-03-18 20:32:15.321402\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"label\",\n  \"content\"\n ],\n \"fields\": [\n  {\n   \"fieldname\": \"label\",\n   \"fieldtype\": \"Data\",\n   \"in_list_view\": 1,\n   \"label\": \"Label\"\n  },\n  {\n   \"fieldname\": \"content\",\n   \"fieldtype\": \"HTML Editor\",\n   \"in_list_view\": 1,\n   \"label\": \"Content\"\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"istable\": 1,\n \"links\": [],\n \"modified\": \"2021-03-18 20:35:26.991192\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Website Item Tabbed Section\",\n \"owner\": \"Administrator\",\n \"permissions\": [],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/website_item_tabbed_section/website_item_tabbed_section.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\n# import frappe\nfrom frappe.model.document import Document\n\n\nclass WebsiteItemTabbedSection(Document):\n\tpass\n"
  },
  {
    "path": "webshop/webshop/doctype/website_offer/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/website_offer/website_offer.json",
    "content": "{\n \"actions\": [],\n \"creation\": \"2021-04-21 13:37:14.162162\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"offer_title\",\n  \"offer_subtitle\",\n  \"offer_details\"\n ],\n \"fields\": [\n  {\n   \"fieldname\": \"offer_title\",\n   \"fieldtype\": \"Data\",\n   \"in_list_view\": 1,\n   \"label\": \"Offer Title\"\n  },\n  {\n   \"fieldname\": \"offer_subtitle\",\n   \"fieldtype\": \"Data\",\n   \"in_list_view\": 1,\n   \"label\": \"Offer Subtitle\"\n  },\n  {\n   \"fieldname\": \"offer_details\",\n   \"fieldtype\": \"Text Editor\",\n   \"label\": \"Offer Details\"\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"istable\": 1,\n \"links\": [],\n \"modified\": \"2021-04-21 13:56:04.660331\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Website Offer\",\n \"owner\": \"Administrator\",\n \"permissions\": [],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/website_offer/website_offer.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport frappe\nfrom frappe.model.document import Document\n\n\nclass WebsiteOffer(Document):\n\tpass\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_offer_details(offer_id):\n\treturn frappe.db.get_value(\"Website Offer\", {\"name\": offer_id}, [\"offer_details\"])\n"
  },
  {
    "path": "webshop/webshop/doctype/wishlist/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/wishlist/test_wishlist.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# See license.txt\nimport unittest\n\nimport frappe\nfrom frappe.core.doctype.user_permission.test_user_permission import create_user\n\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\nfrom webshop.webshop.doctype.wishlist.wishlist import add_to_wishlist, remove_from_wishlist\nfrom erpnext.stock.doctype.item.test_item import make_item\n\n\nclass TestWishlist(unittest.TestCase):\n\tdef setUp(self):\n\t\titem = make_item(\"Test Phone Series X\")\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": \"Test Phone Series X\"}):\n\t\t\tmake_website_item(item, save=True)\n\n\t\titem = make_item(\"Test Phone Series Y\")\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": \"Test Phone Series Y\"}):\n\t\t\tmake_website_item(item, save=True)\n\n\tdef tearDown(self):\n\t\tfrappe.get_cached_doc(\"Website Item\", {\"item_code\": \"Test Phone Series X\"}).delete()\n\t\tfrappe.get_cached_doc(\"Website Item\", {\"item_code\": \"Test Phone Series Y\"}).delete()\n\t\tfrappe.get_cached_doc(\"Item\", \"Test Phone Series X\").delete()\n\t\tfrappe.get_cached_doc(\"Item\", \"Test Phone Series Y\").delete()\n\n\tdef test_add_remove_items_in_wishlist(self):\n\t\t\"Check if items are added and removed from user's wishlist.\"\n\t\t# add first item\n\t\tadd_to_wishlist(\"Test Phone Series X\")\n\n\t\t# check if wishlist was created and item was added\n\t\tself.assertTrue(frappe.db.exists(\"Wishlist\", {\"user\": frappe.session.user}))\n\t\tself.assertTrue(\n\t\t\tfrappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": \"Test Phone Series X\", \"parent\": frappe.session.user}\n\t\t\t)\n\t\t)\n\n\t\t# add second item to wishlist\n\t\tadd_to_wishlist(\"Test Phone Series Y\")\n\t\twishlist_length = frappe.db.get_value(\n\t\t\t\"Wishlist Item\", {\"parent\": frappe.session.user}, \"count(*)\"\n\t\t)\n\t\tself.assertEqual(wishlist_length, 2)\n\n\t\tremove_from_wishlist(\"Test Phone Series X\")\n\t\tremove_from_wishlist(\"Test Phone Series Y\")\n\n\t\twishlist_length = frappe.db.get_value(\n\t\t\t\"Wishlist Item\", {\"parent\": frappe.session.user}, \"count(*)\"\n\t\t)\n\t\tself.assertIsNone(frappe.db.exists(\"Wishlist Item\", {\"parent\": frappe.session.user}))\n\t\tself.assertEqual(wishlist_length, 0)\n\n\t\t# tear down\n\t\tfrappe.get_doc(\"Wishlist\", {\"user\": frappe.session.user}).delete()\n\n\tdef test_add_remove_in_wishlist_multiple_users(self):\n\t\t\"Check if items are added and removed from the correct user's wishlist.\"\n\t\ttest_user = create_user(\"test_reviewer@example.com\", \"Customer\")\n\t\ttest_user_1 = create_user(\"test_reviewer_1@example.com\", \"Customer\")\n\n\t\t# add to wishlist for first user\n\t\tfrappe.set_user(test_user.name)\n\t\tadd_to_wishlist(\"Test Phone Series X\")\n\n\t\t# add to wishlist for second user\n\t\tfrappe.set_user(test_user_1.name)\n\t\tadd_to_wishlist(\"Test Phone Series X\")\n\n\t\t# check wishlist and its content for users\n\t\tself.assertTrue(frappe.db.exists(\"Wishlist\", {\"user\": test_user.name}))\n\t\tself.assertTrue(\n\t\t\tfrappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": \"Test Phone Series X\", \"parent\": test_user.name}\n\t\t\t)\n\t\t)\n\n\t\tself.assertTrue(frappe.db.exists(\"Wishlist\", {\"user\": test_user_1.name}))\n\t\tself.assertTrue(\n\t\t\tfrappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": \"Test Phone Series X\", \"parent\": test_user_1.name}\n\t\t\t)\n\t\t)\n\n\t\t# remove item for second user\n\t\tremove_from_wishlist(\"Test Phone Series X\")\n\n\t\t# make sure item was removed for second user and not first\n\t\tself.assertFalse(\n\t\t\tfrappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": \"Test Phone Series X\", \"parent\": test_user_1.name}\n\t\t\t)\n\t\t)\n\t\tself.assertTrue(\n\t\t\tfrappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": \"Test Phone Series X\", \"parent\": test_user.name}\n\t\t\t)\n\t\t)\n\n\t\t# remove item for first user\n\t\tfrappe.set_user(test_user.name)\n\t\tremove_from_wishlist(\"Test Phone Series X\")\n\t\tself.assertFalse(\n\t\t\tfrappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": \"Test Phone Series X\", \"parent\": test_user.name}\n\t\t\t)\n\t\t)\n\n\t\t# tear down\n\t\tfrappe.set_user(\"Administrator\")\n\t\tfrappe.get_doc(\"Wishlist\", {\"user\": test_user.name}).delete()\n\t\tfrappe.get_doc(\"Wishlist\", {\"user\": test_user_1.name}).delete()\n"
  },
  {
    "path": "webshop/webshop/doctype/wishlist/wishlist.js",
    "content": "// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n// For license information, please see license.txt\n\nfrappe.ui.form.on('Wishlist', {\n\t// refresh: function(frm) {\n\n\t// }\n});\n"
  },
  {
    "path": "webshop/webshop/doctype/wishlist/wishlist.json",
    "content": "{\n \"actions\": [],\n \"autoname\": \"field:user\",\n \"creation\": \"2021-03-10 18:52:28.769126\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"user\",\n  \"section_break_2\",\n  \"items\"\n ],\n \"fields\": [\n  {\n   \"fieldname\": \"user\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"User\",\n   \"options\": \"User\",\n   \"reqd\": 1,\n   \"unique\": 1\n  },\n  {\n   \"fieldname\": \"section_break_2\",\n   \"fieldtype\": \"Section Break\"\n  },\n  {\n   \"fieldname\": \"items\",\n   \"fieldtype\": \"Table\",\n   \"label\": \"Items\",\n   \"options\": \"Wishlist Item\"\n  }\n ],\n \"in_create\": 1,\n \"index_web_pages_for_search\": 1,\n \"links\": [],\n \"modified\": \"2021-07-08 13:11:21.693956\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Wishlist\",\n \"owner\": \"Administrator\",\n \"permissions\": [\n  {\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"System Manager\",\n   \"share\": 1\n  },\n  {\n   \"email\": 1,\n   \"export\": 1,\n   \"print\": 1,\n   \"read\": 1,\n   \"report\": 1,\n   \"role\": \"Website Manager\",\n   \"share\": 1\n  }\n ],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/wishlist/wishlist.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport frappe\nfrom frappe.model.document import Document\n\n\nclass Wishlist(Document):\n\tpass\n\n\n@frappe.whitelist()\ndef add_to_wishlist(item_code):\n\t\"\"\"Insert Item into wishlist.\"\"\"\n\n\tif frappe.db.exists(\"Wishlist Item\", {\"item_code\": item_code, \"parent\": frappe.session.user}):\n\t\treturn\n\n\tweb_item_data = frappe.db.get_value(\n\t\t\"Website Item\",\n\t\t{\"item_code\": item_code},\n\t\t[\n\t\t\t\"website_image\",\n\t\t\t\"website_warehouse\",\n\t\t\t\"name\",\n\t\t\t\"web_item_name\",\n\t\t\t\"item_name\",\n\t\t\t\"item_group\",\n\t\t\t\"route\",\n\t\t],\n\t\tas_dict=1,\n\t)\n\n\twished_item_dict = {\n\t\t\"item_code\": item_code,\n\t\t\"item_name\": web_item_data.get(\"item_name\"),\n\t\t\"item_group\": web_item_data.get(\"item_group\"),\n\t\t\"website_item\": web_item_data.get(\"name\"),\n\t\t\"web_item_name\": web_item_data.get(\"web_item_name\"),\n\t\t\"image\": web_item_data.get(\"website_image\"),\n\t\t\"warehouse\": web_item_data.get(\"website_warehouse\"),\n\t\t\"route\": web_item_data.get(\"route\"),\n\t}\n\n\tif not frappe.db.exists(\"Wishlist\", frappe.session.user):\n\t\t# initialise wishlist\n\t\twishlist = frappe.get_doc({\"doctype\": \"Wishlist\"})\n\t\twishlist.user = frappe.session.user\n\t\twishlist.append(\"items\", wished_item_dict)\n\t\twishlist.save(ignore_permissions=True)\n\telse:\n\t\twishlist = frappe.get_doc(\"Wishlist\", frappe.session.user)\n\t\titem = wishlist.append(\"items\", wished_item_dict)\n\t\titem.db_insert()\n\n\tif hasattr(frappe.local, \"cookie_manager\"):\n\t\tfrappe.local.cookie_manager.set_cookie(\"wish_count\", str(len(wishlist.items)))\n\n\n@frappe.whitelist()\ndef remove_from_wishlist(item_code):\n\tif frappe.db.exists(\"Wishlist Item\", {\"item_code\": item_code, \"parent\": frappe.session.user}):\n\t\tfrappe.db.delete(\"Wishlist Item\", {\"item_code\": item_code, \"parent\": frappe.session.user})\n\t\tfrappe.db.commit()  # nosemgrep\n\n\t\twishlist_items = frappe.db.get_values(\"Wishlist Item\", filters={\"parent\": frappe.session.user})\n\n\t\tif hasattr(frappe.local, \"cookie_manager\"):\n\t\t\tfrappe.local.cookie_manager.set_cookie(\"wish_count\", str(len(wishlist_items)))\n"
  },
  {
    "path": "webshop/webshop/doctype/wishlist_item/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/doctype/wishlist_item/wishlist_item.json",
    "content": "{\n \"actions\": [],\n \"creation\": \"2021-03-10 19:03:00.662714\",\n \"doctype\": \"DocType\",\n \"editable_grid\": 1,\n \"engine\": \"InnoDB\",\n \"field_order\": [\n  \"item_code\",\n  \"website_item\",\n  \"web_item_name\",\n  \"column_break_3\",\n  \"item_name\",\n  \"item_group\",\n  \"item_details_section\",\n  \"description\",\n  \"column_break_7\",\n  \"route\",\n  \"image\",\n  \"image_view\",\n  \"section_break_8\",\n  \"warehouse_section\",\n  \"warehouse\"\n ],\n \"fields\": [\n  {\n   \"fetch_from\": \"website_item.item_code\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"item_code\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Item Code\",\n   \"options\": \"Item\",\n   \"reqd\": 1\n  },\n  {\n   \"fieldname\": \"website_item\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"column_break_3\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.item_name\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"item_name\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Item Name\",\n   \"read_only\": 1\n  },\n  {\n   \"collapsible\": 1,\n   \"fieldname\": \"item_details_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Item Details\",\n   \"read_only\": 1\n  },\n  {\n   \"fetch_from\": \"item_code.description\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"description\",\n   \"fieldtype\": \"Text Editor\",\n   \"label\": \"Description\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"column_break_7\",\n   \"fieldtype\": \"Column Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.image\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"image\",\n   \"fieldtype\": \"Attach\",\n   \"hidden\": 1,\n   \"label\": \"Image\"\n  },\n  {\n   \"fetch_from\": \"item_code.image\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"image_view\",\n   \"fieldtype\": \"Image\",\n   \"hidden\": 1,\n   \"label\": \"Image View\",\n   \"options\": \"image\",\n   \"print_hide\": 1\n  },\n  {\n   \"fieldname\": \"warehouse_section\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Warehouse\"\n  },\n  {\n   \"fieldname\": \"warehouse\",\n   \"fieldtype\": \"Link\",\n   \"in_list_view\": 1,\n   \"label\": \"Warehouse\",\n   \"options\": \"Warehouse\",\n   \"read_only\": 1\n  },\n  {\n   \"fieldname\": \"section_break_8\",\n   \"fieldtype\": \"Section Break\"\n  },\n  {\n   \"fetch_from\": \"item_code.item_group\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"item_group\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"read_only\": 1\n  },\n  {\n   \"fetch_from\": \"website_item.route\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"route\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Route\",\n   \"read_only\": 1\n  },\n  {\n   \"fetch_from\": \"website_item.web_item_name\",\n   \"fetch_if_empty\": 1,\n   \"fieldname\": \"web_item_name\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Website Item Name\",\n   \"read_only\": 1\n  }\n ],\n \"index_web_pages_for_search\": 1,\n \"istable\": 1,\n \"links\": [],\n \"modified\": \"2021-08-09 10:30:41.964802\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Wishlist Item\",\n \"owner\": \"Administrator\",\n \"permissions\": [],\n \"sort_field\": \"modified\",\n \"sort_order\": \"DESC\",\n \"track_changes\": 1\n}"
  },
  {
    "path": "webshop/webshop/doctype/wishlist_item/wishlist_item.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\n# import frappe\nfrom frappe.model.document import Document\n\n\nclass WishlistItem(Document):\n\tpass\n"
  },
  {
    "path": "webshop/webshop/legacy_search.py",
    "content": "import frappe\nfrom frappe.search.full_text_search import FullTextSearch\nfrom frappe.utils import strip_html_tags\nfrom whoosh.analysis import StemmingAnalyzer\nfrom whoosh.fields import ID, KEYWORD, TEXT, Schema\nfrom whoosh.qparser import FieldsPlugin, MultifieldParser, WildcardPlugin\nfrom whoosh.query import Prefix\n\n# TODO: Make obsolete\nINDEX_NAME = \"products\"\n\n\nclass ProductSearch(FullTextSearch):\n\t\"\"\"Wrapper for WebsiteSearch\"\"\"\n\n\tdef get_schema(self):\n\t\treturn Schema(\n\t\t\ttitle=TEXT(stored=True, field_boost=1.5),\n\t\t\tname=ID(stored=True),\n\t\t\tpath=ID(stored=True),\n\t\t\tcontent=TEXT(stored=True, analyzer=StemmingAnalyzer()),\n\t\t\tkeywords=KEYWORD(stored=True, scorable=True, commas=True),\n\t\t)\n\n\tdef get_id(self):\n\t\treturn \"name\"\n\n\tdef get_items_to_index(self):\n\t\t\"\"\"Get all routes to be indexed, this includes the static pages\n\t\tin www/ and routes from published documents\n\n\t\tReturns:\n\t\t        self (object): FullTextSearch Instance\n\t\t\"\"\"\n\t\titems = get_all_published_items()\n\t\tdocuments = [self.get_document_to_index(item) for item in items]\n\t\treturn documents\n\n\tdef get_document_to_index(self, item):\n\t\ttry:\n\t\t\titem = frappe.get_doc(\"Item\", item)\n\t\t\ttitle = item.item_name\n\t\t\tkeywords = [item.item_group]\n\n\t\t\tif item.brand:\n\t\t\t\tkeywords.append(item.brand)\n\n\t\t\tif item.website_image_alt:\n\t\t\t\tkeywords.append(item.website_image_alt)\n\n\t\t\tif item.has_variants and item.variant_based_on == \"Item Attribute\":\n\t\t\t\tkeywords = keywords + [attr.attribute for attr in item.attributes]\n\n\t\t\tif item.web_long_description:\n\t\t\t\tcontent = strip_html_tags(item.web_long_description)\n\t\t\telif item.description:\n\t\t\t\tcontent = strip_html_tags(item.description)\n\n\t\t\treturn frappe._dict(\n\t\t\t\ttitle=title,\n\t\t\t\tname=item.name,\n\t\t\t\tpath=item.route,\n\t\t\t\tcontent=content,\n\t\t\t\tkeywords=\", \".join(keywords),\n\t\t\t)\n\t\texcept Exception:\n\t\t\tpass\n\n\tdef search(self, text, scope=None, limit=20):\n\t\t\"\"\"Search from the current index\n\n\t\tArgs:\n\t\t        text (str): String to search for\n\t\t        scope (str, optional): Scope to limit the search. Defaults to None.\n\t\t        limit (int, optional): Limit number of search results. Defaults to 20.\n\n\t\tReturns:\n\t\t        [List(_dict)]: Search results\n\t\t\"\"\"\n\t\tix = self.get_index()\n\n\t\tresults = None\n\t\tout = []\n\n\t\twith ix.searcher() as searcher:\n\t\t\tparser = MultifieldParser([\"title\", \"content\", \"keywords\"], ix.schema)\n\t\t\tparser.remove_plugin_class(FieldsPlugin)\n\t\t\tparser.remove_plugin_class(WildcardPlugin)\n\t\t\tquery = parser.parse(text)\n\n\t\t\tfilter_scoped = None\n\t\t\tif scope:\n\t\t\t\tfilter_scoped = Prefix(self.id, scope)\n\t\t\tresults = searcher.search(query, limit=limit, filter=filter_scoped)\n\n\t\t\tfor r in results:\n\t\t\t\tout.append(self.parse_result(r))\n\n\t\treturn out\n\n\tdef parse_result(self, result):\n\t\ttitle_highlights = result.highlights(\"title\")\n\t\tcontent_highlights = result.highlights(\"content\")\n\t\tkeyword_highlights = result.highlights(\"keywords\")\n\n\t\treturn frappe._dict(\n\t\t\ttitle=result[\"title\"],\n\t\t\tpath=result[\"path\"],\n\t\t\tkeywords=result[\"keywords\"],\n\t\t\ttitle_highlights=title_highlights,\n\t\t\tcontent_highlights=content_highlights,\n\t\t\tkeyword_highlights=keyword_highlights,\n\t\t)\n\n\ndef get_all_published_items():\n\treturn frappe.get_all(\n\t\t\"Website Item\", filters={\"variant_of\": \"\", \"published\": 1}, pluck=\"item_code\"\n\t)\n\n\ndef update_index_for_path(path):\n\tsearch = ProductSearch(INDEX_NAME)\n\treturn search.update_index_by_name(path)\n\n\ndef remove_document_from_index(path):\n\tsearch = ProductSearch(INDEX_NAME)\n\treturn search.remove_document_from_index(path)\n\n\ndef build_index_for_all_routes():\n\tsearch = ProductSearch(INDEX_NAME)\n\treturn search.build()\n"
  },
  {
    "path": "webshop/webshop/product_data_engine/filters.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\nimport frappe\nfrom frappe import _\nfrom frappe.utils import floor\n\n\nclass ProductFiltersBuilder:\n\tdef __init__(self, item_group=None):\n\t\tif not item_group:\n\t\t\tself.doc = frappe.get_doc(\"Webshop Settings\")\n\t\telse:\n\t\t\tself.doc = frappe.get_doc(\"Item Group\", item_group)\n\n\t\tself.item_group = item_group\n\n\tdef get_field_filters(self):\n\t\tfrom webshop.webshop.doctype.override_doctype.item_group import get_child_groups_for_website\n\n\t\tif not self.item_group and not self.doc.enable_field_filters:\n\t\t\treturn\n\n\t\tfields, filter_data = [], []\n\t\tfilter_fields = [row.fieldname for row in self.doc.filter_fields]  # fields in settings\n\n\t\t# filter valid field filters i.e. those that exist in Website Item\n\t\tweb_item_meta = frappe.get_meta(\"Website Item\", cached=True)\n\t\tfields = [\n\t\t\tweb_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)\n\t\t]\n\n\t\tfor df in fields:\n\t\t\titem_filters, item_or_filters = {\"published\": 1}, []\n\t\t\tlink_doctype_values = self.get_filtered_link_doctype_records(df)\n\n\t\t\tif df.fieldtype == \"Link\":\n\t\t\t\tif self.item_group:\n\t\t\t\t\tinclude_child = frappe.db.get_value(\"Item Group\", self.item_group, \"include_descendants\")\n\t\t\t\t\tif include_child:\n\t\t\t\t\t\tinclude_groups = get_child_groups_for_website(self.item_group, include_self=True)\n\t\t\t\t\t\tinclude_groups = [x.name for x in include_groups]\n\t\t\t\t\t\titem_or_filters.extend(\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t[\"item_group\", \"in\", include_groups],\n\t\t\t\t\t\t\t\t[\"Website Item Group\", \"item_group\", \"=\", self.item_group],  # consider website item groups\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t)\n\t\t\t\t\telse:\n\t\t\t\t\t\titem_or_filters.extend(\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t[\"item_group\", \"=\", self.item_group],\n\t\t\t\t\t\t\t\t[\"Website Item Group\", \"item_group\", \"=\", self.item_group],  # consider website item groups\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t)\n\n\t\t\t\t# exclude variants if mentioned in settings\n\t\t\t\tif frappe.db.get_single_value(\"Webshop Settings\", \"hide_variants\"):\n\t\t\t\t\titem_filters[\"variant_of\"] = [\"is\", \"not set\"]\n\n\t\t\t\t# Get link field values attached to published items\n\t\t\t\titem_values = frappe.get_all(\n\t\t\t\t\t\"Website Item\",\n\t\t\t\t\tfields=[df.fieldname],\n\t\t\t\t\tfilters=item_filters,\n\t\t\t\t\tor_filters=item_or_filters,\n\t\t\t\t\tdistinct=\"True\",\n\t\t\t\t\tpluck=df.fieldname,\n\t\t\t\t)\n\n\t\t\t\tvalues = list(set(item_values) & link_doctype_values)  # intersection of both\n\t\t\telse:\n\t\t\t\t# table multiselect\n\t\t\t\tvalues = list(link_doctype_values)\n\n\t\t\t# Remove None\n\t\t\tif None in values:\n\t\t\t\tvalues.remove(None)\n\n\t\t\tif values:\n\t\t\t\tfilter_data.append([df, values])\n\n\t\treturn filter_data\n\n\tdef get_filtered_link_doctype_records(self, field):\n\t\t\"\"\"\n\t\tGet valid link doctype records depending on filters.\n\t\tApply enable/disable/show_in_website filter.\n\t\tReturns:\n\t\t        set: A set containing valid record names\n\t\t\"\"\"\n\t\tlink_doctype = field.get_link_doctype()\n\t\tmeta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None\n\t\tif meta:\n\t\t\tfilters = self.get_link_doctype_filters(meta)\n\t\t\tlink_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters))\n\n\t\treturn link_doctype_values if meta else set()\n\n\tdef get_link_doctype_filters(self, meta):\n\t\t\"Filters for Link Doctype eg. 'show_in_website'.\"\n\t\tfilters = {}\n\t\tif not meta:\n\t\t\treturn filters\n\n\t\tif meta.has_field(\"enabled\"):\n\t\t\tfilters[\"enabled\"] = 1\n\t\tif meta.has_field(\"disabled\"):\n\t\t\tfilters[\"disabled\"] = 0\n\t\tif meta.has_field(\"show_in_website\"):\n\t\t\tfilters[\"show_in_website\"] = 1\n\n\t\treturn filters\n\n\tdef get_attribute_filters(self):\n\t\tif not self.item_group and not self.doc.enable_attribute_filters:\n\t\t\treturn\n\n\t\tattributes = [row.attribute for row in self.doc.filter_attributes]\n\n\t\tif not attributes:\n\t\t\treturn []\n\n\t\tresult = frappe.get_all(\n\t\t\t\"Item Variant Attribute\",\n\t\t\tfilters={\"attribute\": [\"in\", attributes], \"attribute_value\": [\"is\", \"set\"]},\n\t\t\tfields=[\"attribute\", \"attribute_value\"],\n\t\t\tdistinct=True,\n\t\t)\n\n\t\tattribute_value_map = {}\n\t\tfor d in result:\n\t\t\tattribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)\n\n\t\tout = []\n\t\tfor attribute in attributes:\n\t\t\tif attribute not in attribute_value_map:\n\t\t\t\tcontinue\n\n\t\t\tvalues = attribute_value_map[attribute]\n\t\t\tout.append(frappe._dict(name=attribute, item_attribute_values=values))\n\n\t\treturn out\n\n\tdef get_discount_filters(self, discounts):\n\t\tdiscount_filters = []\n\n\t\t# [25.89, 60.5] min max\n\t\tmin_discount, max_discount = discounts[0], discounts[1]\n\t\t# [25, 60] rounded min max\n\t\tmin_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount)\n\n\t\tmin_range = int(min_discount - (min_range_absolute % 10))  # 20\n\t\tmax_range = int(max_discount - (max_range_absolute % 10))  # 60\n\n\t\tmin_range = (\n\t\t\t(min_range + 10) if min_range != min_range_absolute else min_range\n\t\t)  # 30 (upper limit of 25.89 in range of 10)\n\t\tmax_range = (max_range + 10) if max_range != max_range_absolute else max_range  # 60\n\n\t\tfor discount in range(min_range, (max_range + 1), 10):\n\t\t\tlabel = _(\"{0}% and below\").format(discount)\n\t\t\tdiscount_filters.append([discount, label])\n\n\t\treturn discount_filters\n"
  },
  {
    "path": "webshop/webshop/product_data_engine/query.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nimport frappe\nfrom frappe.utils import flt\n\nfrom webshop.webshop.doctype.item_review.item_review import get_customer\nfrom webshop.webshop.shopping_cart.product_info import get_product_info_for_website\nfrom webshop.webshop.utils.product import get_non_stock_item_status\n\n\nclass ProductQuery:\n\t\"\"\"Query engine for product listing\n\n\tAttributes:\n\t        fields (list): Fields to fetch in query\n\t        conditions (string): Conditions for query building\n\t        or_conditions (string): Search conditions\n\t        page_length (Int): Length of page for the query\n\t        settings (Document): Webshop Settings DocType\n\t\"\"\"\n\n\tdef __init__(self):\n\t\tself.settings = frappe.get_doc(\"Webshop Settings\")\n\t\tself.page_length = self.settings.products_per_page or 20\n\n\t\tself.or_filters = []\n\t\tself.filters = [[\"published\", \"=\", 1]]\n\t\tself.fields = [\n\t\t\t\"web_item_name\",\n\t\t\t\"name\",\n\t\t\t\"item_name\",\n\t\t\t\"item_code\",\n\t\t\t\"website_image\",\n\t\t\t\"variant_of\",\n\t\t\t\"has_variants\",\n\t\t\t\"item_group\",\n\t\t\t\"web_long_description\",\n\t\t\t\"short_description\",\n\t\t\t\"route\",\n\t\t\t\"website_warehouse\",\n\t\t\t\"ranking\",\n\t\t\t\"on_backorder\",\n\t\t]\n\n\tdef query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):\n\t\t\"\"\"\n\t\tArgs:\n\t\t        attributes (dict, optional): Item Attribute filters\n\t\t        fields (dict, optional): Field level filters\n\t\t        search_term (str, optional): Search term to lookup\n\t\t        start (int, optional): Page start\n\n\t\tReturns:\n\t\t        dict: Dict containing items, item count & discount range\n\t\t\"\"\"\n\t\t# track if discounts included in field filters\n\t\tself.filter_with_discount = bool(fields.get(\"discount\"))\n\t\tresult, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0\n\n\t\tif fields:\n\t\t\tself.build_fields_filters(fields)\n\t\tif item_group:\n\t\t\tself.build_item_group_filters(item_group)\n\t\tif search_term:\n\t\t\tself.build_search_filters(search_term)\n\t\tif self.settings.hide_variants:\n\t\t\tself.filters.append([\"variant_of\", \"is\", \"not set\"])\n\n\t\t# query results\n\t\tif attributes:\n\t\t\tresult, count = self.query_items_with_attributes(attributes, start)\n\t\telse:\n\t\t\tresult, count = self.query_items(start=start)\n\n\t\t# sort combined results by ranking\n\t\tresult = sorted(result, key=lambda x: x.get(\"ranking\"), reverse=True)\n\n\t\tif self.settings.enabled:\n\t\t\tcart_items = self.get_cart_items()\n\n\t\tresult, discount_list = self.add_display_details(result, discount_list, cart_items)\n\n\t\tdiscounts = []\n\t\tif discount_list:\n\t\t\tdiscounts = [min(discount_list), max(discount_list)]\n\n\t\tresult = self.filter_results_by_discount(fields, result)\n\n\t\treturn {\"items\": result, \"items_count\": count, \"discounts\": discounts}\n\n\tdef query_items(self, start=0):\n\t\t\"\"\"Build a query to fetch Website Items based on field filters.\"\"\"\n\t\t# MySQL does not support offset without limit,\n\t\t# frappe does not accept two parameters for limit\n\t\t# https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989\n\t\tcount_items = frappe.db.get_all(\n\t\t\t\"Website Item\",\n\t\t\tfilters=self.filters,\n\t\t\tor_filters=self.or_filters,\n\t\t\tlimit_page_length=184467440737095516,\n\t\t\tlimit_start=start,  # get all items from this offset for total count ahead\n\t\t\torder_by=\"ranking desc\",\n\t\t)\n\t\tcount = len(count_items)\n\n\t\t# If discounts included, return all rows.\n\t\t# Slice after filtering rows with discount (See `filter_results_by_discount`).\n\t\t# Slicing before hand will miss discounted items on the 3rd or 4th page.\n\t\t# Discounts are fetched on computing Pricing Rules so we cannot query them directly.\n\t\tpage_length = 184467440737095516 if self.filter_with_discount else self.page_length\n\n\t\titems = frappe.db.get_all(\n\t\t\t\"Website Item\",\n\t\t\tfields=self.fields,\n\t\t\tfilters=self.filters,\n\t\t\tor_filters=self.or_filters,\n\t\t\tlimit_page_length=page_length,\n\t\t\tlimit_start=start,\n\t\t\torder_by=\"ranking desc\",\n\t\t)\n\n\t\treturn items, count\n\n\tdef query_items_with_attributes(self, attributes, start=0):\n\t\t\"\"\"Build a query to fetch Website Items based on field & attribute filters.\"\"\"\n\t\titem_codes = []\n\n\t\tfor attribute, values in attributes.items():\n\t\t\tif not isinstance(values, list):\n\t\t\t\tvalues = [values]\n\n\t\t\t# get items that have selected attribute & value\n\t\t\titem_code_list = frappe.db.get_all(\n\t\t\t\t\"Item\",\n\t\t\t\tfields=[\"item_code\"],\n\t\t\t\tfilters=[\n\t\t\t\t\t[\"published_in_website\", \"=\", 1],\n\t\t\t\t\t[\"Item Variant Attribute\", \"attribute\", \"=\", attribute],\n\t\t\t\t\t[\"Item Variant Attribute\", \"attribute_value\", \"in\", values],\n\t\t\t\t],\n\t\t\t)\n\t\t\titem_codes.append({x.item_code for x in item_code_list})\n\n\t\tif item_codes:\n\t\t\titem_codes = list(set.intersection(*item_codes))\n\t\t\tself.filters.append([\"item_code\", \"in\", item_codes])\n\n\t\titems, count = self.query_items(start=start)\n\n\t\treturn items, count\n\n\tdef build_fields_filters(self, filters):\n\t\t\"\"\"Build filters for field values\n\n\t\tArgs:\n\t\t        filters (dict): Filters\n\t\t\"\"\"\n\t\tfor field, values in filters.items():\n\t\t\tif not values or field == \"discount\":\n\t\t\t\tcontinue\n\n\t\t\t# handle multiselect fields in filter addition\n\t\t\tmeta = frappe.get_meta(\"Website Item\", cached=True)\n\t\t\tdf = meta.get_field(field)\n\t\t\tif df.fieldtype == \"Table MultiSelect\":\n\t\t\t\tchild_doctype = df.options\n\t\t\t\tchild_meta = frappe.get_meta(child_doctype, cached=True)\n\t\t\t\tfields = child_meta.get(\"fields\")\n\t\t\t\tif fields:\n\t\t\t\t\tself.filters.append([child_doctype, fields[0].fieldname, \"IN\", values])\n\t\t\telif isinstance(values, list):\n\t\t\t\t# If value is a list use `IN` query\n\t\t\t\tself.filters.append([field, \"in\", values])\n\t\t\telse:\n\t\t\t\t# `=` will be faster than `IN` for most cases\n\t\t\t\tself.filters.append([field, \"=\", values])\n\n\tdef build_item_group_filters(self, item_group):\n\t\t\"Add filters for Item group page and include Website Item Groups.\"\n\t\tfrom webshop.webshop.doctype.override_doctype.item_group import get_child_groups_for_website\n\n\t\titem_group_filters = []\n\n\t\titem_group_filters.append([\"Website Item\", \"item_group\", \"=\", item_group])\n\t\t# Consider Website Item Groups\n\t\titem_group_filters.append([\"Website Item Group\", \"item_group\", \"=\", item_group])\n\n\t\tif frappe.db.get_value(\"Item Group\", item_group, \"include_descendants\"):\n\t\t\t# include child item group's items as well\n\t\t\t# eg. Group Node A, will show items of child 1 and child 2 as well\n\t\t\t# on it's web page\n\t\t\tinclude_groups = get_child_groups_for_website(item_group, include_self=True)\n\t\t\tinclude_groups = [x.name for x in include_groups]\n\n\t\t\titem_group_filters.append([\"Website Item\", \"item_group\", \"in\", include_groups])\n\n\t\tself.or_filters.extend(item_group_filters)\n\n\tdef build_search_filters(self, search_term):\n\t\t\"\"\"Query search term in specified fields\n\n\t\tArgs:\n\t\t        search_term (str): Search candidate\n\t\t\"\"\"\n\t\t# Default fields to search from\n\t\tdefault_fields = {\"item_code\", \"item_name\", \"web_long_description\", \"item_group\"}\n\n\t\t# Get meta search fields\n\t\tmeta = frappe.get_meta(\"Website Item\")\n\t\tmeta_fields = set(meta.get_search_fields())\n\n\t\t# Join the meta fields and default fields set\n\t\tsearch_fields = default_fields.union(meta_fields)\n\t\tif frappe.db.count(\"Website Item\", cache=True) > 50000:\n\t\t\tsearch_fields.discard(\"web_long_description\")\n\n\t\t# Build or filters for query\n\t\tsearch = \"%{}%\".format(search_term)\n\t\tfor field in search_fields:\n\t\t\tself.or_filters.append([field, \"like\", search])\n\n\tdef add_display_details(self, result, discount_list, cart_items):\n\t\t\"\"\"Add price and availability details in result.\"\"\"\n\t\tfor item in result:\n\t\t\tproduct_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get(\n\t\t\t\t\"product_info\"\n\t\t\t)\n\n\t\t\tif product_info and product_info[\"price\"]:\n\t\t\t\t# update/mutate item and discount_list objects\n\t\t\t\tself.get_price_discount_info(item, product_info[\"price\"], discount_list)\n\n\t\t\tif self.settings.show_stock_availability:\n\t\t\t\tself.get_stock_availability(item)\n\n\t\t\titem.in_cart = item.item_code in cart_items\n\n\t\t\titem.wished = False\n\t\t\tif frappe.db.exists(\n\t\t\t\t\"Wishlist Item\", {\"item_code\": item.item_code, \"parent\": frappe.session.user}\n\t\t\t):\n\t\t\t\titem.wished = True\n\n\t\treturn result, discount_list\n\n\tdef get_price_discount_info(self, item, price_object, discount_list):\n\t\t\"\"\"Modify item object and add price details.\"\"\"\n\t\tfields = [\"formatted_mrp\", \"formatted_price\", \"price_list_rate\"]\n\t\tfor field in fields:\n\t\t\titem[field] = price_object.get(field)\n\n\t\tif price_object.get(\"discount_percent\"):\n\t\t\titem.discount_percent = flt(price_object.discount_percent)\n\t\t\tdiscount_list.append(price_object.discount_percent)\n\n\t\tif item.formatted_mrp:\n\t\t\titem.discount = price_object.get(\"formatted_discount_percent\") or price_object.get(\n\t\t\t\t\"formatted_discount_rate\"\n\t\t\t)\n\n\tdef get_stock_availability(self, item):\n\t\t\"\"\"Modify item object and add stock details.\"\"\"\n\t\tfrom webshop.templates.pages.wishlist import (\n\t\t\tget_stock_availability as get_stock_availability_from_template,\n\t\t)\n\n\t\titem.in_stock = False\n\t\twarehouse = item.get(\"website_warehouse\")\n\t\tis_stock_item = frappe.get_cached_value(\"Item\", item.item_code, \"is_stock_item\")\n\n\t\tif item.get(\"on_backorder\"):\n\t\t\treturn\n\n\t\tif not is_stock_item:\n\t\t\tif warehouse:\n\t\t\t\t# product bundle case\n\t\t\t\titem.in_stock = get_non_stock_item_status(item.item_code, \"website_warehouse\")\n\t\t\telse:\n\t\t\t\titem.in_stock = True\n\t\telif warehouse:\n\t\t\t# stock item and has warehouse\n\t\t\titem.in_stock = get_stock_availability_from_template(item.item_code, warehouse)\n\n\tdef get_cart_items(self):\n\t\tcustomer = get_customer(silent=True)\n\t\tif customer:\n\t\t\tquotation = frappe.get_all(\n\t\t\t\t\"Quotation\",\n\t\t\t\tfields=[\"name\"],\n\t\t\t\tfilters={\n\t\t\t\t\t\"party_name\": customer,\n\t\t\t\t\t\"contact_email\": frappe.session.user,\n\t\t\t\t\t\"order_type\": \"Shopping Cart\",\n\t\t\t\t\t\"docstatus\": 0,\n\t\t\t\t},\n\t\t\t\torder_by=\"modified desc\",\n\t\t\t\tlimit_page_length=1,\n\t\t\t)\n\t\t\tif quotation:\n\t\t\t\titems = frappe.get_all(\n\t\t\t\t\t\"Quotation Item\", fields=[\"item_code\"], filters={\"parent\": quotation[0].get(\"name\")}\n\t\t\t\t)\n\t\t\t\titems = [row.item_code for row in items]\n\t\t\t\treturn items\n\n\t\treturn []\n\n\tdef filter_results_by_discount(self, fields, result):\n\t\tif fields and fields.get(\"discount\"):\n\t\t\tdiscount_percent = frappe.utils.flt(fields[\"discount\"][0])\n\t\t\tresult = [\n\t\t\t\trow\n\t\t\t\tfor row in result\n\t\t\t\tif row.get(\"discount_percent\") and row.discount_percent <= discount_percent\n\t\t\t]\n\n\t\tif self.filter_with_discount:\n\t\t\t# no limit was added to results while querying\n\t\t\t# slice results manually\n\t\t\tresult[: self.page_length]\n\n\t\treturn result\n"
  },
  {
    "path": "webshop/webshop/product_data_engine/test_item_group_product_data_engine.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport unittest\n\nimport frappe\n\nfrom webshop.webshop.api import get_product_filter_data\nfrom webshop.webshop.doctype.website_item.test_website_item import create_regular_web_item\n\ntest_dependencies = [\"Item\", \"Item Group\"]\n\n\nclass TestItemGroupProductDataEngine(unittest.TestCase):\n\t\"Test Products & Sub-Category Querying for Product Listing on Item Group Page.\"\n\n\tdef setUp(self):\n\t\titem_codes = [\n\t\t\t(\"Test Mobile A\", \"_Test Item Group B\"),\n\t\t\t(\"Test Mobile B\", \"_Test Item Group B\"),\n\t\t\t(\"Test Mobile C\", \"_Test Item Group B - 1\"),\n\t\t\t(\"Test Mobile D\", \"_Test Item Group B - 1\"),\n\t\t\t(\"Test Mobile E\", \"_Test Item Group B - 2\"),\n\t\t]\n\t\tfor item in item_codes:\n\t\t\titem_code = item[0]\n\t\t\titem_args = {\"item_group\": item[1]}\n\t\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": item_code}):\n\t\t\t\tcreate_regular_web_item(item_code, item_args=item_args)\n\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B - 1\", \"show_in_website\", 1)\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B - 2\", \"show_in_website\", 1)\n\n\t\tfrappe.db.set_single_value(\"Webshop Settings\", \"products_per_page\", 10)\n\n\tdef tearDown(self):\n\t\tfrappe.db.rollback()\n\n\tdef test_product_listing_in_item_group(self):\n\t\t\"Test if only products belonging to the Item Group are fetched.\"\n\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B\", \"include_descendants\", 0)\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {},\n\t\t\t\t\"attribute_filters\": {},\n\t\t\t\t\"start\": 0,\n\t\t\t\t\"item_group\": \"_Test Item Group B\",\n\t\t\t}\n\t\t)\n\n\t\titems = result.get(\"items\")\n\t\titem_codes = [item.get(\"item_code\") for item in items]\n\n\t\tself.assertEqual(len(items), 2)\n\t\tself.assertIn(\"Test Mobile A\", item_codes)\n\t\tself.assertNotIn(\"Test Mobile C\", item_codes)\n\n\tdef test_products_in_multiple_item_groups(self):\n\t\t\"\"\"Test if product is visible on multiple item group pages barring its own.\"\"\"\n\t\twebsite_item = frappe.get_doc(\"Website Item\", {\"item_code\": \"Test Mobile E\"})\n\n\t\t# show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well\n\t\twebsite_item.append(\"website_item_groups\", {\"item_group\": \"_Test Item Group B - 1\"})\n\t\twebsite_item.save()\n\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {},\n\t\t\t\t\"attribute_filters\": {},\n\t\t\t\t\"start\": 0,\n\t\t\t\t\"item_group\": \"_Test Item Group B - 1\",\n\t\t\t}\n\t\t)\n\n\t\titems = result.get(\"items\")\n\t\titem_codes = [item.get(\"item_code\") for item in items]\n\n\t\tself.assertEqual(len(items), 3)\n\t\tself.assertIn(\"Test Mobile E\", item_codes)  # visible in other item groups\n\t\tself.assertIn(\"Test Mobile C\", item_codes)\n\t\tself.assertIn(\"Test Mobile D\", item_codes)\n\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {},\n\t\t\t\t\"attribute_filters\": {},\n\t\t\t\t\"start\": 0,\n\t\t\t\t\"item_group\": \"_Test Item Group B - 2\",\n\t\t\t}\n\t\t)\n\n\t\titems = result.get(\"items\")\n\n\t\tself.assertEqual(len(items), 1)\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test Mobile E\")  # visible in own item group\n\n\tdef test_item_group_with_sub_groups(self):\n\t\t\"Test Valid Sub Item Groups in Item Group Page.\"\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B - 2\", \"show_in_website\", 0)\n\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {},\n\t\t\t\t\"attribute_filters\": {},\n\t\t\t\t\"start\": 0,\n\t\t\t\t\"item_group\": \"_Test Item Group B\",\n\t\t\t}\n\t\t)\n\n\t\tself.assertTrue(bool(result.get(\"sub_categories\")))\n\n\t\tchild_groups = [d.name for d in result.get(\"sub_categories\")]\n\t\t# check if child group is fetched if shown in website\n\t\tself.assertIn(\"_Test Item Group B - 1\", child_groups)\n\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B - 2\", \"show_in_website\", 1)\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {},\n\t\t\t\t\"attribute_filters\": {},\n\t\t\t\t\"start\": 0,\n\t\t\t\t\"item_group\": \"_Test Item Group B\",\n\t\t\t}\n\t\t)\n\t\tchild_groups = [d.name for d in result.get(\"sub_categories\")]\n\n\t\t# check if child group is fetched if shown in website\n\t\tself.assertIn(\"_Test Item Group B - 1\", child_groups)\n\t\tself.assertIn(\"_Test Item Group B - 2\", child_groups)\n\n\tdef test_item_group_page_with_descendants_included(self):\n\t\t\"\"\"\n\t\tTest if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).\n\t\t> _Test Item Group B [Level 1]\n\t\t        > _Test Item Group B - 1 [Level 2]\n\t\t                > _Test Item Group B - 1 - 1 [Level 3]\n\t\t\"\"\"\n\n\t\tif not frappe.db.exists(\"Item Group\", \"_Test Item Group B - 1 - 1\"):\n\t\t\tfrappe.get_doc(\n\t\t\t\t{  # create Level 3 nested child group\n\t\t\t\t\t\"doctype\": \"Item Group\",\n\t\t\t\t\t\"is_group\": 1,\n\t\t\t\t\t\"item_group_name\": \"_Test Item Group B - 1 - 1\",\n\t\t\t\t\t\"parent_item_group\": \"_Test Item Group B - 1\",\n\t\t\t\t}\n\t\t\t).insert()\n\n\t\tcreate_regular_web_item(  # create an item belonging to level 3 item group\n\t\t\t\"Test Mobile F\", item_args={\"item_group\": \"_Test Item Group B - 1 - 1\"}\n\t\t)\n\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B - 1 - 1\", \"show_in_website\", 1)\n\n\t\t# enable 'include descendants' in Level 1\n\t\tfrappe.db.set_value(\"Item Group\", \"_Test Item Group B\", \"include_descendants\", 1)\n\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {},\n\t\t\t\t\"attribute_filters\": {},\n\t\t\t\t\"start\": 0,\n\t\t\t\t\"item_group\": \"_Test Item Group B\",\n\t\t\t}\n\t\t)\n\n\t\titems = result.get(\"items\")\n\t\titem_codes = [item.get(\"item_code\") for item in items]\n\n\t\t# check if all sub groups' items are pulled\n\t\tself.assertEqual(len(items), 6)\n\t\tself.assertIn(\"Test Mobile A\", item_codes)\n\t\tself.assertIn(\"Test Mobile C\", item_codes)\n\t\tself.assertIn(\"Test Mobile E\", item_codes)\n\t\tself.assertIn(\"Test Mobile F\", item_codes)\n"
  },
  {
    "path": "webshop/webshop/product_data_engine/test_product_data_engine.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors\n# For license information, please see license.txt\n\nimport unittest\n\nimport frappe\n\nfrom webshop.webshop.doctype.webshop_settings.test_webshop_settings import (\n\tsetup_webshop_settings,\n)\nfrom webshop.webshop.doctype.website_item.test_website_item import create_regular_web_item\nfrom webshop.webshop.product_data_engine.filters import ProductFiltersBuilder\nfrom webshop.webshop.product_data_engine.query import ProductQuery\n\ntest_dependencies = [\"Item\", \"Item Group\"]\n\n\nclass TestProductDataEngine(unittest.TestCase):\n\t\"Test Products Querying and Filters for Product Listing.\"\n\n\t@classmethod\n\tdef setUpClass(cls):\n\t\titem_codes = [\n\t\t\t(\"Test 11I Laptop\", \"Products\"),  # rank 1\n\t\t\t(\"Test 12I Laptop\", \"Products\"),  # rank 2\n\t\t\t(\"Test 13I Laptop\", \"Products\"),  # rank 3\n\t\t\t(\"Test 14I Laptop\", \"Raw Material\"),  # rank 4\n\t\t\t(\"Test 15I Laptop\", \"Raw Material\"),  # rank 5\n\t\t\t(\"Test 16I Laptop\", \"Raw Material\"),  # rank 6\n\t\t\t(\"Test 17I Laptop\", \"Products\"),  # rank 7\n\t\t]\n\t\tfor index, item in enumerate(item_codes, start=1):\n\t\t\titem_code = item[0]\n\t\t\titem_args = {\"item_group\": item[1]}\n\t\t\tweb_args = {\"ranking\": index}\n\t\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": item_code}):\n\t\t\t\tcreate_regular_web_item(item_code, item_args=item_args, web_args=web_args)\n\n\t\tsetup_webshop_settings(\n\t\t\t{\n\t\t\t\t\"products_per_page\": 4,\n\t\t\t\t\"enable_field_filters\": 1,\n\t\t\t\t\"filter_fields\": [{\"fieldname\": \"item_group\"}],\n\t\t\t\t\"enable_attribute_filters\": 1,\n\t\t\t\t\"filter_attributes\": [{\"attribute\": \"Test Size\"}],\n\t\t\t\t\"company\": \"_Test Company\",\n\t\t\t\t\"enabled\": 1,\n\t\t\t\t\"default_customer_group\": \"_Test Customer Group\",\n\t\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t}\n\t\t)\n\t\tfrappe.local.shopping_cart_settings = None\n\n\t@classmethod\n\tdef tearDownClass(cls):\n\t\tfrappe.db.rollback()\n\n\tdef test_product_list_ordering_and_paging(self):\n\t\t\"Test if website items appear by ranking on different pages.\"\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None)\n\t\titems = result.get(\"items\")\n\n\t\tself.assertIsNotNone(items)\n\t\tself.assertEqual(len(items), 4)\n\t\tself.assertGreater(result.get(\"items_count\"), 4)\n\n\t\t# check if items appear as per ranking set in setUpClass\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test 17I Laptop\")\n\t\tself.assertEqual(items[1].get(\"item_code\"), \"Test 16I Laptop\")\n\t\tself.assertEqual(items[2].get(\"item_code\"), \"Test 15I Laptop\")\n\t\tself.assertEqual(items[3].get(\"item_code\"), \"Test 14I Laptop\")\n\n\t\t# check next page\n\t\tresult = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None)\n\t\titems = result.get(\"items\")\n\n\t\t# check if items appear as per ranking set in setUpClass on next page\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test 13I Laptop\")\n\t\tself.assertEqual(items[1].get(\"item_code\"), \"Test 12I Laptop\")\n\t\tself.assertEqual(items[2].get(\"item_code\"), \"Test 11I Laptop\")\n\n\tdef test_change_product_ranking(self):\n\t\t\"Test if item on second page appear on first if ranking is changed.\"\n\t\titem_code = \"Test 12I Laptop\"\n\t\told_ranking = frappe.db.get_value(\"Website Item\", {\"item_code\": item_code}, \"ranking\")\n\n\t\t# low rank, appears on second page\n\t\tself.assertEqual(old_ranking, 2)\n\n\t\t# set ranking as highest rank\n\t\tfrappe.db.set_value(\"Website Item\", {\"item_code\": item_code}, \"ranking\", 10)\n\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None)\n\t\titems = result.get(\"items\")\n\n\t\t# check if item is the first item on the first page\n\t\tself.assertEqual(items[0].get(\"item_code\"), item_code)\n\t\tself.assertEqual(items[1].get(\"item_code\"), \"Test 17I Laptop\")\n\n\t\t# tear down\n\t\tfrappe.db.set_value(\"Website Item\", {\"item_code\": item_code}, \"ranking\", old_ranking)\n\n\tdef test_product_list_field_filter_builder(self):\n\t\t\"Test if field filters are fetched correctly.\"\n\t\tfrappe.db.set_value(\"Item Group\", \"Raw Material\", \"show_in_website\", 0)\n\n\t\tfilter_engine = ProductFiltersBuilder()\n\t\tfield_filters = filter_engine.get_field_filters()\n\n\t\t# Web Items belonging to 'Products' and 'Raw Material' are available\n\t\t# but only 'Products' has 'show_in_website' enabled\n\t\titem_group_filters = field_filters[0]\n\t\tdocfield = item_group_filters[0]\n\t\tvalid_item_groups = item_group_filters[1]\n\n\t\tself.assertEqual(docfield.options, \"Item Group\")\n\t\tself.assertIn(\"Products\", valid_item_groups)\n\t\tself.assertNotIn(\"Raw Material\", valid_item_groups)\n\n\t\tfrappe.db.set_value(\"Item Group\", \"Raw Material\", \"show_in_website\", 1)\n\t\tfield_filters = filter_engine.get_field_filters()\n\n\t\t#'Products' and 'Raw Materials' both have 'show_in_website' enabled\n\t\titem_group_filters = field_filters[0]\n\t\tdocfield = item_group_filters[0]\n\t\tvalid_item_groups = item_group_filters[1]\n\n\t\tself.assertEqual(docfield.options, \"Item Group\")\n\t\tself.assertIn(\"Products\", valid_item_groups)\n\t\tself.assertIn(\"Raw Material\", valid_item_groups)\n\n\tdef test_product_list_with_field_filter(self):\n\t\t\"Test if field filters are applied correctly.\"\n\t\tfield_filters = {\"item_group\": \"Raw Material\"}\n\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(\n\t\t\tattributes={}, fields=field_filters, search_term=None, start=0, item_group=None\n\t\t)\n\t\titems = result.get(\"items\")\n\n\t\t# check if only 'Raw Material' are fetched in the right order\n\t\tself.assertEqual(len(items), 3)\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test 16I Laptop\")\n\t\tself.assertEqual(items[1].get(\"item_code\"), \"Test 15I Laptop\")\n\n\t# def test_product_list_with_field_filter_table_multiselect(self):\n\t# \tTODO\n\t# \tpass\n\n\tdef test_product_list_attribute_filter_builder(self):\n\t\t\"Test if attribute filters are fetched correctly.\"\n\t\tcreate_variant_web_item()\n\n\t\tfilter_engine = ProductFiltersBuilder()\n\t\tattribute_filter = filter_engine.get_attribute_filters()[0]\n\t\tattribute_values = attribute_filter.item_attribute_values\n\n\t\tself.assertEqual(attribute_filter.name, \"Test Size\")\n\t\tself.assertGreater(len(attribute_values), 0)\n\t\tself.assertIn(\"Large\", attribute_values)\n\n\tdef test_product_list_with_attribute_filter(self):\n\t\t\"Test if attribute filters are applied correctly.\"\n\t\tcreate_variant_web_item()\n\n\t\tattribute_filters = {\"Test Size\": [\"Large\"]}\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(\n\t\t\tattributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None\n\t\t)\n\t\titems = result.get(\"items\")\n\n\t\t# check if only items with Test Size 'Large' are fetched\n\t\tself.assertEqual(len(items), 1)\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test Web Item-L\")\n\n\tdef test_product_list_discount_filter_builder(self):\n\t\t\"Test if discount filters are fetched correctly.\"\n\t\tfrom webshop.webshop.doctype.website_item.test_website_item import (\n\t\t\tmake_web_item_price,\n\t\t\tmake_web_pricing_rule,\n\t\t)\n\n\t\titem_code = \"Test 12I Laptop\"\n\t\tmake_web_item_price(item_code=item_code)\n\t\tmake_web_pricing_rule(title=f\"Test Pricing Rule for {item_code}\", item_code=item_code, selling=1)\n\n\t\tsetup_webshop_settings({\"show_price\": 1})\n\t\tfrappe.local.shopping_cart_settings = None\n\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None)\n\t\tself.assertTrue(bool(result.get(\"discounts\")))\n\n\t\tfilter_engine = ProductFiltersBuilder()\n\t\tdiscount_filters = filter_engine.get_discount_filters(result[\"discounts\"])\n\n\t\tself.assertEqual(len(discount_filters[0]), 2)\n\t\tself.assertEqual(discount_filters[0][0], 10)\n\t\tself.assertEqual(discount_filters[0][1], \"10% and below\")\n\n\tdef test_product_list_with_discount_filters(self):\n\t\t\"Test if discount filters are applied correctly.\"\n\t\tfrom webshop.webshop.doctype.website_item.test_website_item import (\n\t\t\tmake_web_item_price,\n\t\t\tmake_web_pricing_rule,\n\t\t)\n\n\t\tfield_filters = {\"discount\": [10]}\n\n\t\tmake_web_item_price(item_code=\"Test 12I Laptop\")\n\t\tmake_web_pricing_rule(\n\t\t\ttitle=\"Test Pricing Rule for Test 12I Laptop\",  # 10% discount\n\t\t\titem_code=\"Test 12I Laptop\",\n\t\t\tselling=1,\n\t\t)\n\t\tmake_web_item_price(item_code=\"Test 13I Laptop\")\n\t\tmake_web_pricing_rule(\n\t\t\ttitle=\"Test Pricing Rule for Test 13I Laptop\",  # 15% discount\n\t\t\titem_code=\"Test 13I Laptop\",\n\t\t\tdiscount_percentage=15,\n\t\t\tselling=1,\n\t\t)\n\n\t\tsetup_webshop_settings({\"show_price\": 1})\n\t\tfrappe.local.shopping_cart_settings = None\n\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(\n\t\t\tattributes={}, fields=field_filters, search_term=None, start=0, item_group=None\n\t\t)\n\t\titems = result.get(\"items\")\n\n\t\t# check if only product with 10% and below discount are fetched\n\t\tself.assertEqual(len(items), 1)\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test 12I Laptop\")\n\n\tdef test_product_list_with_api(self):\n\t\t\"Test products listing using API.\"\n\t\tfrom webshop.webshop.api import get_product_filter_data\n\n\t\tcreate_variant_web_item()\n\n\t\tresult = get_product_filter_data(\n\t\t\tquery_args={\n\t\t\t\t\"field_filters\": {\"item_group\": \"Products\"},\n\t\t\t\t\"attribute_filters\": {\"Test Size\": [\"Large\"]},\n\t\t\t\t\"start\": 0,\n\t\t\t}\n\t\t)\n\n\t\titems = result.get(\"items\")\n\n\t\tself.assertEqual(len(items), 1)\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test Web Item-L\")\n\n\tdef test_product_list_with_variants(self):\n\t\t\"Test if variants are hideen on hiding variants in settings.\"\n\t\tcreate_variant_web_item()\n\n\t\tsetup_webshop_settings({\"enable_attribute_filters\": 0, \"hide_variants\": 1})\n\t\tfrappe.local.shopping_cart_settings = None\n\n\t\tattribute_filters = {\"Test Size\": [\"Large\"]}\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(\n\t\t\tattributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None\n\t\t)\n\t\titems = result.get(\"items\")\n\n\t\t# check if any variants are fetched even though published variant exists\n\t\tself.assertEqual(len(items), 0)\n\n\t\t# tear down\n\t\tsetup_webshop_settings({\"enable_attribute_filters\": 1, \"hide_variants\": 0})\n\n\tdef test_custom_field_as_filter(self):\n\t\t\"Test if custom field functions as filter correctly.\"\n\t\tfrom frappe.custom.doctype.custom_field.custom_field import create_custom_field\n\n\t\tcreate_custom_field(\n\t\t\t\"Website Item\",\n\t\t\tdict(\n\t\t\t\towner=\"Administrator\",\n\t\t\t\tfieldname=\"supplier\",\n\t\t\t\tlabel=\"Supplier\",\n\t\t\t\tfieldtype=\"Link\",\n\t\t\t\toptions=\"Supplier\",\n\t\t\t\tinsert_after=\"on_backorder\",\n\t\t\t),\n\t\t)\n\n\t\tfrappe.db.set_value(\n\t\t\t\"Website Item\", {\"item_code\": \"Test 11I Laptop\"}, \"supplier\", \"_Test Supplier\"\n\t\t)\n\t\tfrappe.db.set_value(\n\t\t\t\"Website Item\", {\"item_code\": \"Test 12I Laptop\"}, \"supplier\", \"_Test Supplier 1\"\n\t\t)\n\n\t\tsettings = frappe.get_doc(\"Webshop Settings\")\n\t\tsettings.append(\"filter_fields\", {\"fieldname\": \"supplier\"})\n\t\tsettings.save()\n\n\t\tfilter_engine = ProductFiltersBuilder()\n\t\tfield_filters = filter_engine.get_field_filters()\n\t\tcustom_filter = field_filters[1]\n\t\tfilter_values = custom_filter[1]\n\n\t\tself.assertEqual(custom_filter[0].options, \"Supplier\")\n\t\tself.assertEqual(len(filter_values), 2)\n\t\tself.assertIn(\"_Test Supplier\", filter_values)\n\n\t\t# test if custom filter works in query\n\t\tfield_filters = {\"supplier\": \"_Test Supplier 1\"}\n\t\tengine = ProductQuery()\n\t\tresult = engine.query(\n\t\t\tattributes={}, fields=field_filters, search_term=None, start=0, item_group=None\n\t\t)\n\t\titems = result.get(\"items\")\n\n\t\t# check if only 'Raw Material' are fetched in the right order\n\t\tself.assertEqual(len(items), 1)\n\t\tself.assertEqual(items[0].get(\"item_code\"), \"Test 12I Laptop\")\n\n\ndef create_variant_web_item():\n\t\"Create Variant and Template Website Items.\"\n\tfrom erpnext.controllers.item_variant import create_variant\n\tfrom webshop.webshop.doctype.website_item.website_item import make_website_item\n\tfrom erpnext.stock.doctype.item.test_item import make_item\n\n\tmake_item(\n\t\t\"Test Web Item\",\n\t\t{\n\t\t\t\"has_variant\": 1,\n\t\t\t\"variant_based_on\": \"Item Attribute\",\n\t\t\t\"attributes\": [{\"attribute\": \"Test Size\"}],\n\t\t},\n\t)\n\tif not frappe.db.exists(\"Item\", \"Test Web Item-L\"):\n\t\tvariant = create_variant(\"Test Web Item\", {\"Test Size\": \"Large\"})\n\t\tvariant.save()\n\n\tif not frappe.db.exists(\"Website Item\", {\"variant_of\": \"Test Web Item\"}):\n\t\tmake_website_item(variant, save=True)\n"
  },
  {
    "path": "webshop/webshop/redisearch_utils.py",
    "content": "# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nimport json\n\nimport frappe\nfrom frappe import _\nfrom frappe.utils.redis_wrapper import RedisWrapper\nfrom redis import ResponseError\nfrom redis.commands.search.field import TagField, TextField\nfrom redis.commands.search.suggestion import Suggestion\n\ntry:\n\tfrom redis.commands.search.index_definition import IndexDefinition\nexcept ImportError:\n\tfrom redis.commands.search.indexDefinition import IndexDefinition\n\n\nWEBSITE_ITEM_INDEX = \"website_items_index\"\nWEBSITE_ITEM_KEY_PREFIX = \"website_item:\"\nWEBSITE_ITEM_NAME_AUTOCOMPLETE = \"website_items_name_dict\"\nWEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = \"website_items_category_dict\"\n\n\ndef get_indexable_web_fields():\n\t\"Return valid fields from Website Item that can be searched for.\"\n\tweb_item_meta = frappe.get_meta(\"Website Item\", cached=True)\n\tvalid_fields = filter(\n\t\tlambda df: df.fieldtype in (\"Link\", \"Table MultiSelect\", \"Data\", \"Small Text\", \"Text Editor\"),\n\t\tweb_item_meta.fields,\n\t)\n\n\treturn [df.fieldname for df in valid_fields]\n\n\ndef is_redisearch_enabled():\n\t\"Return True only if redisearch is loaded and enabled.\"\n\tis_redisearch_enabled = frappe.db.get_single_value(\"Webshop Settings\", \"is_redisearch_enabled\")\n\treturn is_search_module_loaded() and is_redisearch_enabled\n\n\ndef is_search_module_loaded():\n\ttry:\n\t\tcache = frappe.cache()\n\t\tfor module in cache.module_list():\n\t\t\tif module.get(b\"name\") == b\"search\":\n\t\t\t\treturn True\n\texcept Exception:\n\t\treturn False  # handling older redis versions\n\n\ndef if_redisearch_enabled(function):\n\t\"Decorator to check if Redisearch is enabled.\"\n\n\tdef wrapper(*args, **kwargs):\n\t\tif is_redisearch_enabled():\n\t\t\tfunc = function(*args, **kwargs)\n\t\t\treturn func\n\t\treturn\n\n\treturn wrapper\n\n\ndef make_key(key):\n\treturn frappe.cache().make_key(key)\n\n\n@if_redisearch_enabled\ndef create_website_items_index():\n\t\"Creates Index Definition.\"\n\n\tredis = frappe.cache()\n\tindex = redis.ft(WEBSITE_ITEM_INDEX)\n\n\ttry:\n\t\tindex.dropindex()  # drop if already exists\n\texcept ResponseError:\n\t\t# will most likely raise a ResponseError if index does not exist\n\t\t# ignore and create index\n\t\tpass\n\texcept Exception:\n\t\traise_redisearch_error()\n\n\tidx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])\n\n\t# Index fields mentioned in webshop settings\n\tidx_fields = frappe.db.get_single_value(\"Webshop Settings\", \"search_index_fields\")\n\tidx_fields = idx_fields.split(\",\") if idx_fields else []\n\n\tif \"web_item_name\" in idx_fields:\n\t\tidx_fields.remove(\"web_item_name\")\n\n\tidx_fields = [to_search_field(f) for f in idx_fields]\n\n\t# TODO: sortable?\n\tindex.create_index(\n\t\t[TextField(\"web_item_name\", sortable=True)] + idx_fields,\n\t\tdefinition=idx_def,\n\t)\n\n\treindex_all_web_items()\n\tdefine_autocomplete_dictionary()\n\n\ndef to_search_field(field):\n\tif field == \"tags\":\n\t\treturn TagField(\"tags\", separator=\",\")\n\n\treturn TextField(field)\n\n\n@if_redisearch_enabled\ndef insert_item_to_index(website_item_doc):\n\t# Insert item to index\n\tkey = get_cache_key(website_item_doc.name)\n\tcache = frappe.cache()\n\tweb_item = create_web_item_map(website_item_doc)\n\n\tfor field, value in web_item.items():\n\t\tsuper(RedisWrapper, cache).hset(make_key(key), field, value)\n\n\tinsert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)\n\n\n@if_redisearch_enabled\ndef insert_to_name_ac(web_name, doc_name):\n\tac = frappe.cache().ft()\n\tac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(web_name, payload=doc_name))\n\n\ndef create_web_item_map(website_item_doc):\n\tfields_to_index = get_fields_indexed()\n\tweb_item = {}\n\n\tfor field in fields_to_index:\n\t\tweb_item[field] = website_item_doc.get(field) or \"\"\n\n\treturn web_item\n\n\n@if_redisearch_enabled\ndef update_index_for_item(website_item_doc):\n\t# Reinsert to Cache\n\tinsert_item_to_index(website_item_doc)\n\tdefine_autocomplete_dictionary()\n\n\n@if_redisearch_enabled\ndef delete_item_from_index(website_item_doc):\n\tcache = frappe.cache()\n\tkey = get_cache_key(website_item_doc.name)\n\n\ttry:\n\t\tcache.delete(key)\n\texcept Exception:\n\t\traise_redisearch_error()\n\n\tdelete_from_ac_dict(website_item_doc)\n\treturn True\n\n\n@if_redisearch_enabled\ndef delete_from_ac_dict(website_item_doc):\n\t\"\"\"Removes this items's name from autocomplete dictionary\"\"\"\n\tac = frappe.cache().ft()\n\tac.sugdel(website_item_doc.web_item_name)\n\n\n@if_redisearch_enabled\ndef define_autocomplete_dictionary():\n\t\"\"\"\n\tDefines/Redefines an autocomplete search dictionary for Website Item Name.\n\tAlso creats autocomplete dictionary for Published Item Groups.\n\t\"\"\"\n\n\tcache = frappe.cache()\n\n\t# Delete both autocomplete dicts\n\ttry:\n\t\tcache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))\n\t\tcache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))\n\texcept Exception:\n\t\traise_redisearch_error()\n\n\tcreate_items_autocomplete_dict()\n\tcreate_item_groups_autocomplete_dict()\n\n\n@if_redisearch_enabled\ndef create_items_autocomplete_dict():\n\t\"Add items as suggestions in Autocompleter.\"\n\n\tac = frappe.cache().ft()\n\titems = frappe.get_all(\n\t\t\"Website Item\", fields=[\"web_item_name\", \"item_group\"], filters={\"published\": 1}\n\t)\n\tfor item in items:\n\t\tac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(item.web_item_name))\n\n\n@if_redisearch_enabled\ndef create_item_groups_autocomplete_dict():\n\t\"Add item groups with weightage as suggestions in Autocompleter.\"\n\n\tpublished_item_groups = frappe.get_all(\n\t\t\"Item Group\", fields=[\"name\", \"route\", \"weightage\"], filters={\"show_in_website\": 1}\n\t)\n\tif not published_item_groups:\n\t\treturn\n\n\tac = frappe.cache().ft()\n\n\tfor item_group in published_item_groups:\n\t\tpayload = json.dumps({\"name\": item_group.name, \"route\": item_group.route})\n\t\tac.sugadd(\n\t\t\tWEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,\n\t\t\tSuggestion(\n\t\t\t\tstring=item_group.name,\n\t\t\t\tscore=frappe.utils.flt(item_group.weightage) or 1.0,\n\t\t\t\tpayload=payload,  # additional info that can be retrieved later\n\t\t\t),\n\t\t)\n\n\n@if_redisearch_enabled\ndef reindex_all_web_items():\n\titems = frappe.get_all(\"Website Item\", fields=get_fields_indexed(), filters={\"published\": True})\n\n\tcache = frappe.cache()\n\tfor item in items:\n\t\tweb_item = create_web_item_map(item)\n\t\tkey = make_key(get_cache_key(item.name))\n\n\t\tfor field, value in web_item.items():\n\t\t\tsuper(RedisWrapper, cache).hset(key, field, value)\n\n\ndef get_cache_key(name):\n\tname = frappe.scrub(name)\n\treturn f\"{WEBSITE_ITEM_KEY_PREFIX}{name}\"\n\n\ndef get_fields_indexed():\n\tfields_to_index = frappe.db.get_single_value(\"Webshop Settings\", \"search_index_fields\")\n\tfields_to_index = fields_to_index.split(\",\") if fields_to_index else []\n\n\tmandatory_fields = [\"name\", \"web_item_name\", \"route\", \"thumbnail\", \"ranking\"]\n\tfields_to_index = fields_to_index + mandatory_fields\n\n\treturn fields_to_index\n\n\ndef raise_redisearch_error():\n\t\"Create an Error Log and raise error.\"\n\tlog = frappe.log_error(\"Redisearch Error\")\n\tlog_link = frappe.utils.get_link_to_form(\"Error Log\", log.name)\n\n\tfrappe.throw(\n\t\tmsg=_(\"Something went wrong. Check {0}\").format(log_link), title=_(\"Redisearch Error\")\n\t)\n"
  },
  {
    "path": "webshop/webshop/shopping_cart/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/shopping_cart/cart.py",
    "content": "# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nimport frappe\nimport frappe.defaults\nfrom frappe import _, throw\nfrom frappe.contacts.doctype.address.address import get_address_display\nfrom frappe.contacts.doctype.contact.contact import get_contact_name\nfrom frappe.utils import cint, cstr, flt, get_fullname\nfrom frappe.utils.nestedset import get_root_of\n\nfrom erpnext.accounts.utils import get_account_name\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n    get_shopping_cart_settings,\n)\nfrom webshop.webshop.utils.product import get_web_item_qty_in_stock\nfrom erpnext.selling.doctype.quotation.quotation import _make_sales_order\n\n\nclass WebsitePriceListMissingError(frappe.ValidationError):\n    pass\n\n\ndef set_cart_count(quotation=None):\n\tif cint(frappe.db.get_singles_value(\"Webshop Settings\", \"enabled\")):\n\t\tif not quotation:\n\t\t\tquotation = _get_cart_quotation()\n\t\tcart_count = cstr(cint(quotation.get(\"total_qty\")))\n\n\t\tif hasattr(frappe.local, \"cookie_manager\"):\n\t\t\tfrappe.local.cookie_manager.set_cookie(\"cart_count\", cart_count)\n\n\n@frappe.whitelist()\ndef get_cart_quotation(doc=None):\n\tparty = get_party()\n\n\tif not doc:\n\t\tquotation = _get_cart_quotation(party)\n\t\tdoc = quotation\n\t\tset_cart_count(quotation)\n\n\taddresses = get_address_docs(party=party)\n\n\tif not doc.customer_address and addresses:\n\t\tupdate_cart_address(\"billing\", addresses[0].name)\n\n\treturn {\n\t\t\"doc\": decorate_quotation_doc(doc),\n\t\t\"shipping_addresses\": get_shipping_addresses(party),\n\t\t\"billing_addresses\": get_billing_addresses(party),\n\t\t\"shipping_rules\": get_applicable_shipping_rules(party),\n\t\t\"cart_settings\": frappe.get_cached_doc(\"Webshop Settings\"),\n\t}\n\n\n@frappe.whitelist()\ndef get_shipping_addresses(party=None):\n\tif not party:\n\t\tparty = get_party()\n\taddresses = get_address_docs(party=party)\n\treturn [\n\t\t{\n\t\t\t\"name\": address.name,\n\t\t\t\"title\": address.address_title,\n\t\t\t\"display\": address.display,\n\t\t}\n\t\tfor address in addresses\n\t\tif address.address_type == \"Shipping\"\n\t]\n\n\n@frappe.whitelist()\ndef get_billing_addresses(party=None):\n\tif not party:\n\t\tparty = get_party()\n\taddresses = get_address_docs(party=party)\n\treturn [\n\t\t{\n\t\t\t\"name\": address.name,\n\t\t\t\"title\": address.address_title,\n\t\t\t\"display\": address.display,\n\t\t}\n\t\tfor address in addresses\n\t\tif address.address_type == \"Billing\"\n\t]\n\n\n@frappe.whitelist()\ndef place_order():\n\tquotation = _get_cart_quotation()\n\tcart_settings = frappe.get_cached_doc(\"Webshop Settings\")\n\tquotation.company = cart_settings.company\n\n\tquotation.flags.ignore_permissions = True\n\tquotation.submit()\n\n\tif quotation.quotation_to == \"Lead\" and quotation.party_name:\n\t\t# company used to create customer accounts\n\t\tfrappe.defaults.set_user_default(\"company\", quotation.company)\n\n\tif not (quotation.shipping_address_name or quotation.customer_address):\n\t\tfrappe.throw(_(\"Set Shipping Address or Billing Address\"))\n\n\tsales_order = frappe.get_doc(\n\t\t_make_sales_order(\n\t\t\tquotation.name, ignore_permissions=True\n\t\t)\n\t)\n\tsales_order.payment_schedule = []\n\n\tif not cint(cart_settings.allow_items_not_in_stock):\n\t\tfor item in sales_order.get(\"items\"):\n\t\t\titem.warehouse = frappe.db.get_value(\n\t\t\t\t\"Website Item\", {\"item_code\": item.item_code}, \"website_warehouse\"\n\t\t\t)\n\t\t\tis_stock_item = frappe.db.get_value(\"Item\", item.item_code, \"is_stock_item\")\n\n\t\t\tif is_stock_item:\n\t\t\t\titem_stock = get_web_item_qty_in_stock(\n\t\t\t\t\titem.item_code, \"website_warehouse\"\n\t\t\t\t)\n\t\t\t\tif not cint(item_stock.in_stock):\n\t\t\t\t\tthrow(_(\"{0} Not in Stock\").format(item.item_code))\n\t\t\t\tif item.qty > item_stock.stock_qty:\n\t\t\t\t\tthrow(\n\t\t\t\t\t\t_(\"Only {0} in Stock for item {1}\").format(\n\t\t\t\t\t\t\titem_stock.stock_qty, item.item_code\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\n\tsales_order.flags.ignore_permissions = True\n\tsales_order.insert()\n\tsales_order.submit()\n\n\tif hasattr(frappe.local, \"cookie_manager\"):\n\t\tfrappe.local.cookie_manager.delete_cookie(\"cart_count\")\n\n\treturn sales_order.name\n\n\n@frappe.whitelist()\ndef request_for_quotation():\n\tquotation = _get_cart_quotation()\n\tquotation.flags.ignore_permissions = True\n\n\tif get_shopping_cart_settings().save_quotations_as_draft:\n\t\tquotation.save()\n\telse:\n\t\tquotation.submit()\n\n\treturn quotation.name\n\n\n@frappe.whitelist()\ndef update_cart(item_code, qty, additional_notes=None, with_items=False):\n\tquotation = _get_cart_quotation()\n\n\tempty_card = False\n\tqty = flt(qty)\n\tif qty == 0:\n\t\tquotation_items = quotation.get(\"items\", {\"item_code\": [\"!=\", item_code]})\n\t\tif quotation_items:\n\t\t\tquotation.set(\"items\", quotation_items)\n\t\telse:\n\t\t\tempty_card = True\n\n\telse:\n\t\twarehouse = frappe.get_cached_value(\n\t\t\t\"Website Item\", {\"item_code\": item_code}, \"website_warehouse\"\n\t\t)\n\n\t\tquotation_items = quotation.get(\"items\", {\"item_code\": item_code})\n\t\tif not quotation_items:\n\t\t\tquotation.append(\n\t\t\t\t\"items\",\n\t\t\t\t{\n\t\t\t\t\t\"doctype\": \"Quotation Item\",\n\t\t\t\t\t\"item_code\": item_code,\n\t\t\t\t\t\"qty\": qty,\n\t\t\t\t\t\"additional_notes\": additional_notes,\n\t\t\t\t\t\"warehouse\": warehouse,\n\t\t\t\t},\n\t\t\t)\n\t\telse:\n\t\t\tquotation_items[0].qty = qty\n\t\t\tquotation_items[0].warehouse = warehouse\n\t\t\tquotation_items[0].additional_notes = additional_notes\n\n\tapply_cart_settings(quotation=quotation)\n\n\tquotation.flags.ignore_permissions = True\n\tquotation.payment_schedule = []\n\tif not empty_card:\n\t\tquotation.save()\n\telse:\n\t\tquotation.delete()\n\t\tquotation = None\n\n\tset_cart_count(quotation)\n\n\tif cint(with_items):\n\t\tcontext = get_cart_quotation(quotation)\n\t\treturn {\n\t\t\t\"items\": frappe.render_template(\n\t\t\t\t\"templates/includes/cart/cart_items.html\", context\n\t\t\t),\n\t\t\t\"total\": frappe.render_template(\n\t\t\t\t\"templates/includes/cart/cart_items_total.html\", context\n\t\t\t),\n\t\t\t\"taxes_and_totals\": frappe.render_template(\n\t\t\t\t\"templates/includes/cart/cart_payment_summary.html\", context\n\t\t\t),\n\t\t}\n\telse:\n\t\treturn {\"name\": quotation.name}\n\n\n@frappe.whitelist()\ndef get_shopping_cart_menu(context=None):\n\tif not context:\n\t\tcontext = get_cart_quotation()\n\n\treturn frappe.render_template(\"templates/includes/cart/cart_dropdown.html\", context)\n\n\n@frappe.whitelist()\ndef add_new_address(doc):\n\tdoc = frappe.parse_json(doc)\n\tdoc.update({\"doctype\": \"Address\"})\n\taddress = frappe.get_doc(doc)\n\taddress.save(ignore_permissions=True)\n\n\treturn address\n\n\n@frappe.whitelist(allow_guest=True)\ndef create_lead_for_item_inquiry(lead, subject, message):\n\tlead = frappe.parse_json(lead)\n\tlead_doc = frappe.new_doc(\"Lead\")\n\tfor fieldname in (\"lead_name\", \"company_name\", \"email_id\", \"phone\"):\n\t\tlead_doc.set(fieldname, lead.get(fieldname))\n\n\tlead_doc.set(\"lead_owner\", \"\")\n\n\tif not frappe.db.exists(\"Lead Source\", \"Product Inquiry\"):\n\t\tfrappe.get_doc(\n\t\t\t{\"doctype\": \"Lead Source\", \"source_name\": \"Product Inquiry\"}\n\t\t).insert(ignore_permissions=True)\n\n\tlead_doc.set(\"source\", \"Product Inquiry\")\n\n\ttry:\n\t\tlead_doc.save(ignore_permissions=True)\n\texcept frappe.exceptions.DuplicateEntryError:\n\t\tfrappe.clear_messages()\n\t\tlead_doc = frappe.get_doc(\"Lead\", {\"email_id\": lead[\"email_id\"]})\n\n\tlead_doc.add_comment(\n\t\t\"Comment\",\n\t\ttext=\"\"\"\n\t\t<div>\n\t\t\t<h5>{subject}</h5>\n\t\t\t<p>{message}</p>\n\t\t</div>\n\t\"\"\".format(\n\t\t\tsubject=subject, message=message\n\t\t),\n\t)\n\n\treturn lead_doc\n\n\n@frappe.whitelist()\ndef get_terms_and_conditions(terms_name):\n\treturn frappe.db.get_value(\"Terms and Conditions\", terms_name, \"terms\")\n\n\n@frappe.whitelist()\ndef update_cart_address(address_type, address_name):\n\tquotation = _get_cart_quotation()\n\taddress_doc = frappe.get_doc(\"Address\", address_name).as_dict()\n\taddress_display = get_address_display(address_doc)\n\n\tif address_type.lower() == \"billing\":\n\t\tquotation.customer_address = address_name\n\t\tquotation.address_display = address_display\n\t\tquotation.shipping_address_name = (\n\t\t\tquotation.shipping_address_name or address_name\n\t\t)\n\t\taddress_doc = next(\n\t\t\t(doc for doc in get_billing_addresses() if doc[\"name\"] == address_name),\n\t\t\tNone,\n\t\t)\n\telif address_type.lower() == \"shipping\":\n\t\tquotation.shipping_address_name = address_name\n\t\tquotation.shipping_address = address_display\n\t\tquotation.customer_address = quotation.customer_address or address_name\n\t\taddress_doc = next(\n\t\t\t(doc for doc in get_shipping_addresses() if doc[\"name\"] == address_name),\n\t\t\tNone,\n\t\t)\n\tapply_cart_settings(quotation=quotation)\n\n\tquotation.flags.ignore_permissions = True\n\tquotation.save()\n\n\tcontext = get_cart_quotation(quotation)\n\tcontext[\"address\"] = address_doc\n\n\treturn {\n\t\t\"taxes\": frappe.render_template(\n\t\t\t\"templates/includes/order/order_taxes.html\", context\n\t\t),\n\t\t\"address\": frappe.render_template(\n\t\t\t\"templates/includes/cart/address_card.html\", context\n\t\t),\n\t}\n\n\ndef guess_territory():\n\tterritory = None\n\tgeoip_country = frappe.session.get(\"session_country\")\n\tif geoip_country:\n\t\tterritory = frappe.db.get_value(\"Territory\", geoip_country)\n\n\treturn (\n\t\tterritory\n\t\tor get_root_of(\"Territory\")\n\t)\n\n\ndef decorate_quotation_doc(doc):\n\tfor d in doc.get(\"items\", []):\n\t\titem_code = d.item_code\n\t\tfields = [\"web_item_name\", \"thumbnail\", \"website_image\", \"description\", \"route\"]\n\n\t\t# Variant Item\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": item_code}):\n\t\t\tvariant_data = frappe.db.get_values(\n\t\t\t\t\"Item\",\n\t\t\t\tfilters={\"item_code\": item_code},\n\t\t\t\tfieldname=[\"variant_of\", \"item_name\", \"image\"],\n\t\t\t\tas_dict=True,\n\t\t\t)[0]\n\t\t\titem_code = variant_data.variant_of\n\t\t\tfields = fields[1:]\n\t\t\td.web_item_name = variant_data.item_name\n\n\t\t\tif variant_data.image:  # get image from variant or template web item\n\t\t\t\td.thumbnail = variant_data.image\n\t\t\t\tfields = fields[2:]\n\n\t\td.update(\n\t\t\tfrappe.db.get_value(\n\t\t\t\t\"Website Item\", {\"item_code\": item_code}, fields, as_dict=True\n\t\t\t)\n\t\t)\n\n\t\twebsite_warehouse = frappe.get_cached_value(\n\t\t\t\"Website Item\", {\"item_code\": item_code}, \"website_warehouse\"\n\t\t)\n\n\t\td.warehouse = website_warehouse\n\n\treturn doc\n\n\ndef _get_cart_quotation(party=None):\n\t\"\"\"Return the open Quotation of type \"Shopping Cart\" or make a new one\"\"\"\n\tif not party:\n\t\tparty = get_party()\n\n\tquotation = frappe.get_all(\n\t\t\"Quotation\",\n\t\tfields=[\"name\"],\n\t\tfilters={\n\t\t\t\"party_name\": party.name,\n\t\t\t\"contact_email\": frappe.session.user,\n\t\t\t\"order_type\": \"Shopping Cart\",\n\t\t\t\"docstatus\": 0,\n\t\t},\n\t\torder_by=\"modified desc\",\n\t\tlimit_page_length=1,\n\t)\n\n\tif quotation:\n\t\tqdoc = frappe.get_doc(\"Quotation\", quotation[0].name)\n\telse:\n\t\tcompany = frappe.db.get_single_value(\"Webshop Settings\", \"company\")\n\t\tqdoc = frappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"Quotation\",\n\t\t\t\t\"naming_series\": get_shopping_cart_settings().quotation_series\n\t\t\t\tor \"QTN-CART-\",\n\t\t\t\t\"quotation_to\": party.doctype,\n\t\t\t\t\"company\": company,\n\t\t\t\t\"order_type\": \"Shopping Cart\",\n\t\t\t\t\"status\": \"Draft\",\n\t\t\t\t\"docstatus\": 0,\n\t\t\t\t\"__islocal\": 1,\n\t\t\t\t\"party_name\": party.name,\n\t\t\t}\n\t\t)\n\n\t\tqdoc.contact_person = frappe.db.get_value(\n\t\t\t\"Contact\", {\"email_id\": frappe.session.user}\n\t\t)\n\t\tqdoc.contact_email = frappe.session.user\n\n\t\tqdoc.flags.ignore_permissions = True\n\t\tqdoc.run_method(\"set_missing_values\")\n\t\tapply_cart_settings(party, qdoc)\n\n\treturn qdoc\n\n\ndef update_party(fullname, company_name=None, mobile_no=None, phone=None):\n\tparty = get_party()\n\n\tparty.customer_name = company_name or fullname\n\tparty.customer_type = \"Company\" if company_name else \"Individual\"\n\n\tcontact_name = frappe.db.get_value(\"Contact\", {\"email_id\": frappe.session.user})\n\tcontact = frappe.get_doc(\"Contact\", contact_name)\n\tcontact.first_name = fullname\n\tcontact.last_name = None\n\tcontact.customer_name = party.customer_name\n\tcontact.mobile_no = mobile_no\n\tcontact.phone = phone\n\tcontact.flags.ignore_permissions = True\n\tcontact.save()\n\n\tparty_doc = frappe.get_doc(party.as_dict())\n\tparty_doc.flags.ignore_permissions = True\n\tparty_doc.save()\n\n\tqdoc = _get_cart_quotation(party)\n\tif not qdoc.get(\"__islocal\"):\n\t\tqdoc.customer_name = company_name or fullname\n\t\tqdoc.run_method(\"set_missing_lead_customer_details\")\n\t\tqdoc.flags.ignore_permissions = True\n\t\tqdoc.save()\n\n\ndef apply_cart_settings(party=None, quotation=None):\n\tif not party:\n\t\tparty = get_party()\n\tif not quotation:\n\t\tquotation = _get_cart_quotation(party)\n\n\tcart_settings = frappe.get_cached_doc(\"Webshop Settings\")\n\n\tset_price_list_and_rate(quotation, cart_settings)\n\n\tquotation.run_method(\"calculate_taxes_and_totals\")\n\n\tset_taxes(quotation, cart_settings)\n\n\t_apply_shipping_rule(party, quotation, cart_settings)\n\n\ndef set_price_list_and_rate(quotation, cart_settings):\n\t\"\"\"set price list based on billing territory\"\"\"\n\n\t_set_price_list(cart_settings, quotation)\n\n\t# reset values\n\tquotation.price_list_currency = (\n\t\tquotation.currency\n\t) = quotation.plc_conversion_rate = quotation.conversion_rate = None\n\tfor item in quotation.get(\"items\"):\n\t\titem.price_list_rate = item.discount_percentage = item.rate = item.amount = None\n\n\t# refetch values\n\tquotation.run_method(\"set_price_list_and_item_details\")\n\n\tif hasattr(frappe.local, \"cookie_manager\"):\n\t\t# set it in cookies for using in product page\n\t\tfrappe.local.cookie_manager.set_cookie(\n\t\t\t\"selling_price_list\", quotation.selling_price_list\n\t\t)\n\n\ndef _set_price_list(cart_settings, quotation=None):\n\t\"\"\"Set price list based on customer or shopping cart default\"\"\"\n\tfrom erpnext.accounts.party import get_default_price_list\n\n\tparty_name = quotation.get(\"party_name\") if quotation else get_party().get(\"name\")\n\tselling_price_list = None\n\n\t# check if default customer price list exists\n\tif party_name and frappe.db.exists(\"Customer\", party_name):\n\t\tselling_price_list = get_default_price_list(\n\t\t\tfrappe.get_doc(\"Customer\", party_name)\n\t\t)\n\n\t# check default price list in shopping cart\n\tif not selling_price_list:\n\t\tselling_price_list = cart_settings.price_list\n\n\tif quotation:\n\t\tquotation.selling_price_list = selling_price_list\n\n\treturn selling_price_list\n\n\ndef set_taxes(quotation, cart_settings):\n\t\"\"\"set taxes based on billing territory\"\"\"\n\tfrom erpnext.accounts.party import set_taxes\n\n\tcustomer_group = frappe.db.get_value(\n\t\t\"Customer\", quotation.party_name, \"customer_group\"\n\t)\n\n\tquotation.taxes_and_charges = set_taxes(\n\t\tquotation.party_name,\n\t\t\"Customer\",\n\t\tquotation.transaction_date,\n\t\tquotation.company,\n\t\tcustomer_group=customer_group,\n\t\tsupplier_group=None,\n\t\ttax_category=quotation.tax_category,\n\t\tbilling_address=quotation.customer_address,\n\t\tshipping_address=quotation.shipping_address_name,\n\t\tuse_for_shopping_cart=1,\n\t)\n\t#\n\t# \t# clear table\n\tquotation.set(\"taxes\", [])\n\t#\n\t# \t# append taxes\n\tquotation.append_taxes_from_master()\n\tquotation.append_taxes_from_item_tax_template()\n\n\ndef get_party(user=None):\n\tif not user:\n\t\tuser = frappe.session.user\n\n\tcontact_name = get_contact_name(user)\n\tparty = None\n\n\tif contact_name:\n\t\tcontact = frappe.get_doc(\"Contact\", contact_name)\n\t\tif contact.links:\n\t\t\tparty_doctype = contact.links[0].link_doctype\n\t\t\tparty = contact.links[0].link_name\n\n\tcart_settings = frappe.get_cached_doc(\"Webshop Settings\")\n\n\tdebtors_account = \"\"\n\n\tif cart_settings.enable_checkout:\n\t\tdebtors_account = get_debtors_account(cart_settings)\n\n\tif party:\n\t\tdoc = frappe.get_doc(party_doctype, party)\n\t\tif doc.doctype in [\"Customer\", \"Supplier\"]:\n\t\t\tif not frappe.db.exists(\"Portal User\", {\"parent\": doc.name, \"user\": user}):\n\t\t\t\tdoc.append(\"portal_users\", {\"user\": user})\n\t\t\t\tdoc.flags.ignore_permissions = True\n\t\t\t\tdoc.flags.ignore_mandatory = True\n\t\t\t\tdoc.save()\n\n\t\treturn doc\n\n\telif not frappe.db.exists(\"Portal User\", {\"user\": user}):\n\t\tif not cart_settings.enabled:\n\t\t\tfrappe.local.flags.redirect_location = \"/contact\"\n\t\t\traise frappe.Redirect\n\t\tcustomer = frappe.new_doc(\"Customer\")\n\t\tfullname = get_fullname(user)\n\t\tcustomer.update(\n\t\t\t{\n\t\t\t\t\"customer_name\": fullname,\n\t\t\t\t\"customer_type\": \"Individual\",\n\t\t\t\t\"customer_group\": get_shopping_cart_settings().default_customer_group,\n\t\t\t\t\"territory\": get_root_of(\"Territory\"),\n\t\t\t}\n\t\t)\n\n\t\tcustomer.append(\"portal_users\", {\"user\": user})\n\n\t\tif debtors_account:\n\t\t\tcustomer.update(\n\t\t\t\t{\n\t\t\t\t\t\"accounts\": [\n\t\t\t\t\t\t{\"company\": cart_settings.company, \"account\": debtors_account}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t)\n\n\t\tcustomer.flags.ignore_mandatory = True\n\t\tcustomer.insert(ignore_permissions=True)\n\n\t\tcontact = frappe.new_doc(\"Contact\")\n\t\tcontact.update(\n\t\t\t{\"first_name\": fullname, \"email_ids\": [{\"email_id\": user, \"is_primary\": 1}]}\n\t\t)\n\t\tcontact.append(\"links\", dict(link_doctype=\"Customer\", link_name=customer.name))\n\t\tcontact.flags.ignore_mandatory = True\n\t\tcontact.insert(ignore_permissions=True)\n\n\t\treturn customer\n\telse:\n\t\tcustomer = frappe.db.get_value(\n\t\t\t\"Portal User\", {\"user\": user}, [\"parent\"]\n\t\t)\n\n\t\tif frappe.db.exists(\"Customer\", customer):\n\t\t\treturn frappe.get_doc(\"Customer\", customer)\n\n\ndef get_debtors_account(cart_settings):\n\tif not cart_settings.payment_gateway_account:\n\t\tfrappe.throw(_(\"Payment Gateway Account not set\"), _(\"Mandatory\"))\n\n\tpayment_gateway_account_currency = frappe.get_doc(\n\t\t\"Payment Gateway Account\", cart_settings.payment_gateway_account\n\t).currency\n\n\taccount_name = _(\"Debtors ({0})\").format(payment_gateway_account_currency)\n\n\tdebtors_account_name = get_account_name(\n\t\t\"Receivable\",\n\t\t\"Asset\",\n\t\tis_group=0,\n\t\taccount_currency=payment_gateway_account_currency,\n\t\tcompany=cart_settings.company,\n\t)\n\n\tif not debtors_account_name:\n\t\tdebtors_account = frappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"Account\",\n\t\t\t\t\"account_type\": \"Receivable\",\n\t\t\t\t\"root_type\": \"Asset\",\n\t\t\t\t\"is_group\": 0,\n\t\t\t\t\"parent_account\": get_account_name(\n\t\t\t\t\troot_type=\"Asset\", is_group=1, company=cart_settings.company\n\t\t\t\t),\n\t\t\t\t\"account_name\": account_name,\n\t\t\t\t\"currency\": payment_gateway_account_currency,\n\t\t\t}\n\t\t).insert(ignore_permissions=True)\n\n\t\treturn debtors_account.name\n\n\telse:\n\t\treturn debtors_account_name\n\n\ndef get_address_docs(\n    doctype=None,\n    txt=None,\n    filters=None,\n    limit_start=0,\n    limit_page_length=20,\n    party=None,\n):\n\tif not party:\n\t\tparty = get_party()\n\n\tif not party:\n\t\treturn []\n\n\taddress_names = frappe.db.get_all(\n\t\t\"Dynamic Link\",\n\t\tfields=(\"parent\"),\n\t\tfilters=dict(\n\t\t\tparenttype=\"Address\", link_doctype=party.doctype, link_name=party.name\n\t\t),\n\t)\n\n\tout = []\n\n\tfor a in address_names:\n\t\taddress = frappe.get_doc(\"Address\", a.parent)\n\t\taddress.display = get_address_display(address.as_dict())\n\t\tout.append(address)\n\n\treturn out\n\n\n@frappe.whitelist()\ndef apply_shipping_rule(shipping_rule):\n\tquotation = _get_cart_quotation()\n\n\tquotation.shipping_rule = shipping_rule\n\n\tapply_cart_settings(quotation=quotation)\n\n\tquotation.flags.ignore_permissions = True\n\tquotation.save()\n\n\treturn get_cart_quotation(quotation)\n\n\ndef _apply_shipping_rule(party=None, quotation=None, cart_settings=None):\n\tif not quotation.shipping_rule:\n\t\tshipping_rules = get_shipping_rules(quotation, cart_settings)\n\n\t\tif not shipping_rules:\n\t\t\treturn\n\n\t\telif quotation.shipping_rule not in shipping_rules:\n\t\t\tquotation.shipping_rule = shipping_rules[0]\n\n\tif quotation.shipping_rule:\n\t\tquotation.run_method(\"apply_shipping_rule\")\n\t\tquotation.run_method(\"calculate_taxes_and_totals\")\n\n\ndef get_applicable_shipping_rules(party=None, quotation=None):\n\tshipping_rules = get_shipping_rules(quotation)\n\n\tif shipping_rules:\n\t\trule_label_map = frappe.db.get_values(\"Shipping Rule\", shipping_rules, \"label\")\n\t\t# we need this in sorted order as per the position of the rule in the settings page\n\t\treturn [[rule, rule] for rule in shipping_rules]\n\n\ndef get_shipping_rules(quotation=None, cart_settings=None):\n\tif not quotation:\n\t\tquotation = _get_cart_quotation()\n\n\tshipping_rules = []\n\tif quotation.shipping_address_name:\n\t\tcountry = frappe.db.get_value(\n\t\t\t\"Address\", quotation.shipping_address_name, \"country\"\n\t\t)\n\t\tif country:\n\t\t\tsr_country = frappe.qb.DocType(\"Shipping Rule Country\")\n\t\t\tsr = frappe.qb.DocType(\"Shipping Rule\")\n\t\t\tquery = (\n\t\t\t\tfrappe.qb.from_(sr_country)\n\t\t\t\t.join(sr)\n\t\t\t\t.on(sr.name == sr_country.parent)\n\t\t\t\t.select(sr.name)\n\t\t\t\t.distinct()\n\t\t\t\t.where((sr_country.country == country) & (sr.disabled != 1) & (sr.shipping_rule_type == \"Selling\"))\n\t\t\t)\n\t\t\tresult = query.run(as_list=True)\n\t\t\tshipping_rules = [x[0] for x in result]\n\n\treturn shipping_rules\n\n\ndef get_address_territory(address_name):\n\t\"\"\"Tries to match city, state and country of address to existing territory\"\"\"\n\tterritory = None\n\n\tif address_name:\n\t\taddress_fields = frappe.db.get_value(\n\t\t\t\"Address\", address_name, [\"city\", \"state\", \"country\"]\n\t\t)\n\t\tfor value in address_fields:\n\t\t\tterritory = frappe.db.get_value(\"Territory\", value)\n\t\t\tif territory:\n\t\t\t\tbreak\n\n\treturn territory\n\n\ndef show_terms(doc):\n\treturn doc.tc_name\n\n\n@frappe.whitelist(allow_guest=True)\ndef apply_coupon_code(applied_code, applied_referral_sales_partner):\n\tquotation = True\n\n\tif not applied_code:\n\t\tfrappe.throw(_(\"Please enter a coupon code\"))\n\n\tcoupon_list = frappe.get_all(\"Coupon Code\", filters={\"coupon_code\": applied_code})\n\tif not coupon_list:\n\t\tfrappe.throw(_(\"Please enter a valid coupon code\"))\n\n\tcoupon_name = coupon_list[0].name\n\n\tfrom erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code\n\n\tvalidate_coupon_code(coupon_name)\n\tquotation = _get_cart_quotation()\n\tquotation.ignore_pricing_rule = 0\n\tquotation.coupon_code = coupon_name\n\tquotation.flags.ignore_permissions = True\n\tquotation.save()\n\n\tif applied_referral_sales_partner:\n\t\tsales_partner_list = frappe.get_all(\n\t\t\t\"Sales Partner\", filters={\"referral_code\": applied_referral_sales_partner}\n\t\t)\n\t\tif sales_partner_list:\n\t\t\tsales_partner_name = sales_partner_list[0].name\n\t\t\tquotation.referral_sales_partner = sales_partner_name\n\t\t\tquotation.flags.ignore_permissions = True\n\t\t\tquotation.save()\n\n\treturn quotation\n\n \n@frappe.whitelist(allow_guest=True)\ndef remove_coupon_code():\n\tquotation = _get_cart_quotation()\n\tquotation.coupon_code = \"\"\n\tquotation.referral_sales_partner = \"\"\n\tquotation.flags.ignore_permissions = True\n\n\t# reset discount amount if coupon code is removed (on desk it is done in client side)\n\t# as we are enabling ignore_pricing_rule, so we also need to manually reset discount percentage\n\tquotation.discount_amount = 0\n\tquotation.additional_discount_percentage = 0\n\tquotation.ignore_pricing_rule = 1\n\n\tquotation.save()\n\n\treturn quotation"
  },
  {
    "path": "webshop/webshop/shopping_cart/product_info.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\nimport frappe\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n    get_shopping_cart_settings,\n    show_quantity_in_website,\n)\nfrom webshop.webshop.shopping_cart.cart import _get_cart_quotation, _set_price_list\nfrom erpnext.utilities.product import (get_price)\nfrom webshop.webshop.utils.product import (get_non_stock_item_status, get_web_item_qty_in_stock)\nfrom webshop.webshop.shopping_cart.cart import get_party\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_product_info_for_website(item_code, skip_quotation_creation=False):\n\t\"\"\"\n\tGet product price / stock info for website\n\t\"\"\"\n\n\tcart_settings = get_shopping_cart_settings()\n\tif not cart_settings.enabled:\n\t\t# return settings even if cart is disabled\n\t\treturn frappe._dict({\"product_info\": {}, \"cart_settings\": cart_settings})\n\n\tcart_quotation = frappe._dict()\n\tif not skip_quotation_creation:\n\t\tcart_quotation = _get_cart_quotation()\n\n\tselling_price_list = (\n\t\tcart_quotation.get(\"selling_price_list\")\n\t\tif cart_quotation\n\t\telse _set_price_list(cart_settings, None)\n\t)\n\n\tprice = {}\n\tif cart_settings.show_price:\n\t\tis_guest = frappe.session.user == \"Guest\"\n\t\tparty = get_party()\n\n\t\t# Show Price if logged in.\n\t\t# If not logged in, check if price is hidden for guest.\n\t\tif not is_guest or not cart_settings.hide_price_for_guest:\n\t\t\tprice = get_price(\n\t\t\t\titem_code,\n\t\t\t\tselling_price_list,\n\t\t\t\tcart_settings.default_customer_group,\n\t\t\t\tcart_settings.company,\n\t\t\t\tparty=party,\n\t\t\t)\n\n\tstock_status = None\n\n\tif cart_settings.show_stock_availability:\n\t\ton_backorder = frappe.get_cached_value(\n\t\t\t\"Website Item\", {\"item_code\": item_code}, \"on_backorder\"\n\t\t)\n\t\tif on_backorder:\n\t\t\tstock_status = frappe._dict({\"on_backorder\": True})\n\t\telse:\n\t\t\tstock_status = get_web_item_qty_in_stock(item_code, \"website_warehouse\")\n\n\tproduct_info = {\n\t\t\"price\": price,\n\t\t\"qty\": 0,\n\t\t\"uom\": frappe.db.get_value(\"Item\", item_code, \"stock_uom\"),\n\t\t\"sales_uom\": frappe.db.get_value(\"Item\", item_code, \"sales_uom\"),\n\t}\n\n\tif stock_status:\n\t\tif stock_status.on_backorder:\n\t\t\tproduct_info[\"on_backorder\"] = True\n\t\telse:\n\t\t\tproduct_info[\"stock_qty\"] = stock_status.stock_qty\n\t\t\tproduct_info[\"in_stock\"] = (\n\t\t\t\tstock_status.in_stock\n\t\t\t\tif stock_status.is_stock_item\n\t\t\t\telse get_non_stock_item_status(item_code, \"website_warehouse\")\n\t\t\t)\n\t\t\tproduct_info[\"show_stock_qty\"] = show_quantity_in_website()\n\n\tif product_info[\"price\"]:\n\t\tif frappe.session.user != \"Guest\":\n\t\t\titem = (\n\t\t\t\tcart_quotation.get({\"item_code\": item_code}) if cart_quotation else None\n\t\t\t)\n\t\t\tif item:\n\t\t\t\tproduct_info[\"qty\"] = item[0].qty\n\n\treturn frappe._dict({\"product_info\": product_info, \"cart_settings\": cart_settings})\n\n\ndef set_product_info_for_website(item):\n\t\"\"\"set product price uom for website\"\"\"\n\tproduct_info = get_product_info_for_website(\n\t\titem.item_code, skip_quotation_creation=True\n\t).get(\"product_info\")\n\n\tif product_info:\n\t\titem.update(product_info)\n\t\titem[\"stock_uom\"] = product_info.get(\"uom\")\n\t\titem[\"sales_uom\"] = product_info.get(\"sales_uom\")\n\t\tif product_info.get(\"price\"):\n\t\t\titem[\"price_stock_uom\"] = product_info.get(\"price\").get(\"formatted_price\")\n\t\t\titem[\"price_sales_uom\"] = product_info.get(\"price\").get(\n\t\t\t\t\"formatted_price_sales_uom\"\n\t\t\t)\n\t\telse:\n\t\t\titem[\"price_stock_uom\"] = \"\"\n\t\t\titem[\"price_sales_uom\"] = \"\"\n"
  },
  {
    "path": "webshop/webshop/shopping_cart/test_shopping_cart.py",
    "content": "# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\n\n\nimport unittest\n\nimport frappe\nfrom frappe.tests.utils import change_settings\nfrom frappe.utils import add_months, cint, nowdate\n\nfrom erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\nfrom webshop.webshop.shopping_cart.cart import (\n\t_get_cart_quotation,\n\tget_cart_quotation,\n\tget_party,\n\trequest_for_quotation,\n\tupdate_cart,\n)\nfrom erpnext.tests.utils import create_test_contact_and_address\n\n\nclass TestShoppingCart(unittest.TestCase):\n\t\"\"\"\n\tNote:\n\tShopping Cart == Quotation\n\t\"\"\"\n\n\tdef setUp(self):\n\t\tfrappe.set_user(\"Administrator\")\n\t\tself.enable_shopping_cart()\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": \"_Test Item\"}):\n\t\t\tmake_website_item(frappe.get_cached_doc(\"Item\", \"_Test Item\"))\n\n\t\tif not frappe.db.exists(\"Website Item\", {\"item_code\": \"_Test Item 2\"}):\n\t\t\tmake_website_item(frappe.get_cached_doc(\"Item\", \"_Test Item 2\"))\n\n\tdef tearDown(self):\n\t\tfrappe.db.rollback()\n\t\tfrappe.set_user(\"Administrator\")\n\t\tself.disable_shopping_cart()\n\n\t@classmethod\n\tdef tearDownClass(cls):\n\t\tfrappe.db.sql(\"delete from `tabTax Rule`\")\n\n\tdef test_get_cart_new_user(self):\n\t\tself.login_as_customer(\n\t\t\t\"test_contact_two_customer@example.com\", \"_Test Contact 2 For _Test Customer\"\n\t\t)\n\t\tcreate_address_and_contact(\n\t\t\taddress_title=\"_Test Address for Customer 2\",\n\t\t\tfirst_name=\"_Test Contact for Customer 2\",\n\t\t\temail=\"test_contact_two_customer@example.com\",\n\t\t\tcustomer=\"_Test Customer 2\",\n\t\t)\n\t\t# test if lead is created and quotation with new lead is fetched\n\t\tcustomer = frappe.get_doc(\"Customer\", \"_Test Customer 2\")\n\t\tquotation = _get_cart_quotation(party=customer)\n\t\tself.assertEqual(quotation.quotation_to, \"Customer\")\n\t\tself.assertEqual(\n\t\t\tquotation.contact_person,\n\t\t\tfrappe.db.get_value(\"Contact\", dict(email_id=\"test_contact_two_customer@example.com\")),\n\t\t)\n\t\tself.assertEqual(quotation.contact_email, frappe.session.user)\n\n\t\treturn quotation\n\n\tdef test_get_cart_customer(self, customer=\"_Test Customer 2\"):\n\t\tdef validate_quotation(customer_name):\n\t\t\t# test if quotation with customer is fetched\n\t\t\tparty = frappe.get_doc(\"Customer\", customer_name)\n\t\t\tquotation = _get_cart_quotation(party=party)\n\t\t\tself.assertEqual(quotation.quotation_to, \"Customer\")\n\t\t\tself.assertEqual(quotation.party_name, customer_name)\n\t\t\tself.assertEqual(quotation.contact_email, frappe.session.user)\n\t\t\treturn quotation\n\n\t\tquotation = validate_quotation(customer)\n\t\treturn quotation\n\n\tdef test_add_to_cart(self):\n\t\tself.login_as_customer(\n\t\t\t\"test_contact_two_customer@example.com\", \"_Test Contact 2 For _Test Customer\"\n\t\t)\n\t\tcreate_address_and_contact(\n\t\t\taddress_title=\"_Test Address for Customer 2\",\n\t\t\tfirst_name=\"_Test Contact for Customer 2\",\n\t\t\temail=\"test_contact_two_customer@example.com\",\n\t\t\tcustomer=\"_Test Customer 2\",\n\t\t)\n\t\t# clear existing quotations\n\t\tself.clear_existing_quotations()\n\n\t\t# add first item\n\t\tupdate_cart(\"_Test Item\", 1)\n\n\t\tquotation = self.test_get_cart_customer(\"_Test Customer 2\")\n\n\t\tself.assertEqual(quotation.get(\"items\")[0].item_code, \"_Test Item\")\n\t\tself.assertEqual(quotation.get(\"items\")[0].qty, 1)\n\t\tself.assertEqual(quotation.get(\"items\")[0].amount, 10)\n\n\t\t# add second item\n\t\tupdate_cart(\"_Test Item 2\", 1)\n\t\tquotation = self.test_get_cart_customer(\"_Test Customer 2\")\n\t\tself.assertEqual(quotation.get(\"items\")[1].item_code, \"_Test Item 2\")\n\t\tself.assertEqual(quotation.get(\"items\")[1].qty, 1)\n\t\tself.assertEqual(quotation.get(\"items\")[1].amount, 20)\n\n\t\tself.assertEqual(len(quotation.get(\"items\")), 2)\n\n\tdef test_update_cart(self):\n\t\t# first, add to cart\n\t\tself.test_add_to_cart()\n\n\t\t# update first item\n\t\tupdate_cart(\"_Test Item\", 5)\n\t\tquotation = self.test_get_cart_customer(\"_Test Customer 2\")\n\t\tself.assertEqual(quotation.get(\"items\")[0].item_code, \"_Test Item\")\n\t\tself.assertEqual(quotation.get(\"items\")[0].qty, 5)\n\t\tself.assertEqual(quotation.get(\"items\")[0].amount, 50)\n\t\tself.assertEqual(quotation.net_total, 70)\n\t\tself.assertEqual(len(quotation.get(\"items\")), 2)\n\n\tdef test_remove_from_cart(self):\n\t\t# first, add to cart\n\t\tself.test_add_to_cart()\n\n\t\t# remove first item\n\t\tupdate_cart(\"_Test Item\", 0)\n\t\tquotation = self.test_get_cart_customer(\"_Test Customer 2\")\n\n\t\tself.assertEqual(quotation.get(\"items\")[0].item_code, \"_Test Item 2\")\n\t\tself.assertEqual(quotation.get(\"items\")[0].qty, 1)\n\t\tself.assertEqual(quotation.get(\"items\")[0].amount, 20)\n\t\tself.assertEqual(quotation.net_total, 20)\n\t\tself.assertEqual(len(quotation.get(\"items\")), 1)\n\n\t@unittest.skip(\"Flaky in CI\")\n\tdef test_tax_rule(self):\n\t\tself.create_tax_rule()\n\n\t\tself.login_as_customer(\n\t\t\t\"test_contact_two_customer@example.com\", \"_Test Contact 2 For _Test Customer\"\n\t\t)\n\t\tcreate_address_and_contact(\n\t\t\taddress_title=\"_Test Address for Customer 2\",\n\t\t\tfirst_name=\"_Test Contact for Customer 2\",\n\t\t\temail=\"test_contact_two_customer@example.com\",\n\t\t\tcustomer=\"_Test Customer 2\",\n\t\t)\n\n\t\tquotation = self.create_quotation()\n\n\t\tfrom erpnext.accounts.party import set_taxes\n\n\t\ttax_rule_master = set_taxes(\n\t\t\tquotation.party_name,\n\t\t\t\"Customer\",\n\t\t\tNone,\n\t\t\tquotation.company,\n\t\t\tcustomer_group=None,\n\t\t\tsupplier_group=None,\n\t\t\ttax_category=quotation.tax_category,\n\t\t\tbilling_address=quotation.customer_address,\n\t\t\tshipping_address=quotation.shipping_address_name,\n\t\t\tuse_for_shopping_cart=1,\n\t\t)\n\n\t\tself.assertEqual(quotation.taxes_and_charges, tax_rule_master)\n\t\tself.assertEqual(quotation.total_taxes_and_charges, 1000.0)\n\n\t\tself.remove_test_quotation(quotation)\n\n\t@change_settings(\n\t\t\"Webshop Settings\",\n\t\t{\n\t\t\t\"company\": \"_Test Company\",\n\t\t\t\"enabled\": 1,\n\t\t\t\"default_customer_group\": \"_Test Customer Group\",\n\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t\"show_price\": 1,\n\t\t},\n\t)\n\tdef test_add_item_variant_without_web_item_to_cart(self):\n\t\t\"Test adding Variants having no Website Items in cart via Template Web Item.\"\n\t\tfrom erpnext.controllers.item_variant import create_variant\n\t\tfrom erpnext.stock.doctype.item.test_item import make_item\n\n\t\ttemplate_item = make_item(\n\t\t\t\"Test-Tshirt-Temp\",\n\t\t\t{\n\t\t\t\t\"has_variant\": 1,\n\t\t\t\t\"variant_based_on\": \"Item Attribute\",\n\t\t\t\t\"attributes\": [{\"attribute\": \"Test Size\"}, {\"attribute\": \"Test Colour\"}],\n\t\t\t},\n\t\t)\n\t\tvariant = create_variant(\"Test-Tshirt-Temp\", {\"Test Size\": \"Small\", \"Test Colour\": \"Red\"})\n\t\tvariant.save()\n\t\tmake_website_item(template_item)  # publish template not variant\n\n\t\tupdate_cart(\"Test-Tshirt-Temp-S-R\", 1)\n\n\t\tcart = get_cart_quotation()  # test if cart page gets data without errors\n\t\tdoc = cart.get(\"doc\")\n\n\t\tself.assertEqual(doc.get(\"items\")[0].item_name, \"Test-Tshirt-Temp-S-R\")\n\n\t\t# test if items are rendered without error\n\t\tfrappe.render_template(\"templates/includes/cart/cart_items.html\", cart)\n\n\t@change_settings(\"Webshop Settings\", {\"save_quotations_as_draft\": 1})\n\tdef test_cart_without_checkout_and_draft_quotation(self):\n\t\t\"Test impact of 'save_quotations_as_draft' checkbox.\"\n\t\tfrappe.local.shopping_cart_settings = None\n\n\t\t# add item to cart\n\t\tupdate_cart(\"_Test Item\", 1)\n\t\tquote_name = request_for_quotation()  # Request for Quote\n\t\tquote_doctstatus = cint(frappe.db.get_value(\"Quotation\", quote_name, \"docstatus\"))\n\n\t\tself.assertEqual(quote_doctstatus, 0)\n\n\t\tfrappe.db.set_single_value(\"Webshop Settings\", \"save_quotations_as_draft\", 0)\n\t\tfrappe.local.shopping_cart_settings = None\n\t\tupdate_cart(\"_Test Item\", 1)\n\t\tquote_name = request_for_quotation()  # Request for Quote\n\t\tquote_doctstatus = cint(frappe.db.get_value(\"Quotation\", quote_name, \"docstatus\"))\n\n\t\tself.assertEqual(quote_doctstatus, 1)\n\n\tdef create_tax_rule(self):\n\t\ttax_rule = frappe.get_test_records(\"Tax Rule\")[0]\n\t\ttry:\n\t\t\tfrappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)\n\t\texcept (frappe.DuplicateEntryError, ConflictingTaxRule):\n\t\t\tpass\n\n\tdef create_quotation(self):\n\t\tquotation = frappe.new_doc(\"Quotation\")\n\n\t\tvalues = {\n\t\t\t\"doctype\": \"Quotation\",\n\t\t\t\"quotation_to\": \"Customer\",\n\t\t\t\"order_type\": \"Shopping Cart\",\n\t\t\t\"party_name\": get_party(frappe.session.user).name,\n\t\t\t\"docstatus\": 0,\n\t\t\t\"contact_email\": frappe.session.user,\n\t\t\t\"selling_price_list\": \"_Test Price List Rest of the World\",\n\t\t\t\"currency\": \"USD\",\n\t\t\t\"taxes_and_charges\": \"_Test Tax 1 - _TC\",\n\t\t\t\"conversion_rate\": 1,\n\t\t\t\"transaction_date\": nowdate(),\n\t\t\t\"valid_till\": add_months(nowdate(), 1),\n\t\t\t\"items\": [{\"item_code\": \"_Test Item\", \"qty\": 1}],\n\t\t\t\"taxes\": frappe.get_doc(\"Sales Taxes and Charges Template\", \"_Test Tax 1 - _TC\").taxes,\n\t\t\t\"company\": \"_Test Company\",\n\t\t}\n\n\t\tquotation.update(values)\n\n\t\tquotation.insert(ignore_permissions=True)\n\n\t\treturn quotation\n\n\tdef remove_test_quotation(self, quotation):\n\t\tfrappe.set_user(\"Administrator\")\n\t\tquotation.delete()\n\n\t# helper functions\n\tdef enable_shopping_cart(self):\n\t\tsettings = frappe.get_doc(\"Webshop Settings\")\n\n\t\tsettings.update(\n\t\t\t{\n\t\t\t\t\"enabled\": 1,\n\t\t\t\t\"company\": \"_Test Company\",\n\t\t\t\t\"default_customer_group\": \"_Test Customer Group\",\n\t\t\t\t\"quotation_series\": \"_T-Quotation-\",\n\t\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t}\n\t\t)\n\n\t\t# insert item price\n\t\tif not frappe.db.get_value(\n\t\t\t\"Item Price\", {\"price_list\": \"_Test Price List India\", \"item_code\": \"_Test Item\"}\n\t\t):\n\t\t\tfrappe.get_doc(\n\t\t\t\t{\n\t\t\t\t\t\"doctype\": \"Item Price\",\n\t\t\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t\t\t\"item_code\": \"_Test Item\",\n\t\t\t\t\t\"price_list_rate\": 10,\n\t\t\t\t}\n\t\t\t).insert()\n\t\t\tfrappe.get_doc(\n\t\t\t\t{\n\t\t\t\t\t\"doctype\": \"Item Price\",\n\t\t\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t\t\t\"item_code\": \"_Test Item 2\",\n\t\t\t\t\t\"price_list_rate\": 20,\n\t\t\t\t}\n\t\t\t).insert()\n\n\t\tsettings.save()\n\t\tfrappe.local.shopping_cart_settings = None\n\n\tdef disable_shopping_cart(self):\n\t\tsettings = frappe.get_doc(\"Webshop Settings\")\n\t\tsettings.enabled = 0\n\t\tsettings.save()\n\t\tfrappe.local.shopping_cart_settings = None\n\n\tdef login_as_new_user(self):\n\t\tself.create_user_if_not_exists(\"test_cart_user@example.com\")\n\t\tfrappe.set_user(\"test_cart_user@example.com\")\n\n\tdef login_as_customer(\n\t\tself, email=\"test_contact_customer@example.com\", name=\"_Test Contact For _Test Customer\"\n\t):\n\t\tself.create_user_if_not_exists(email, name)\n\t\tfrappe.set_user(email)\n\n\tdef clear_existing_quotations(self):\n\t\tquotations = frappe.get_all(\n\t\t\t\"Quotation\",\n\t\t\tfilters={\"party_name\": get_party().name, \"order_type\": \"Shopping Cart\", \"docstatus\": 0},\n\t\t\torder_by=\"modified desc\",\n\t\t\tpluck=\"name\",\n\t\t)\n\n\t\tfor quotation in quotations:\n\t\t\tfrappe.delete_doc(\"Quotation\", quotation, ignore_permissions=True, force=True)\n\n\tdef create_user_if_not_exists(self, email, first_name=None):\n\t\tif frappe.db.exists(\"User\", email):\n\t\t\treturn\n\n\t\tuser = frappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"User\",\n\t\t\t\t\"user_type\": \"Website User\",\n\t\t\t\t\"email\": email,\n\t\t\t\t\"send_welcome_email\": 0,\n\t\t\t\t\"first_name\": first_name or email.split(\"@\")[0],\n\t\t\t}\n\t\t).insert(ignore_permissions=True)\n\n\t\tuser.add_roles(\"Customer\")\n\n\ndef create_address_and_contact(**kwargs):\n\tif not frappe.db.get_value(\"Address\", {\"address_title\": kwargs.get(\"address_title\")}):\n\t\tfrappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"Address\",\n\t\t\t\t\"address_title\": kwargs.get(\"address_title\"),\n\t\t\t\t\"address_type\": kwargs.get(\"address_type\") or \"Office\",\n\t\t\t\t\"address_line1\": kwargs.get(\"address_line1\") or \"Station Road\",\n\t\t\t\t\"city\": kwargs.get(\"city\") or \"_Test City\",\n\t\t\t\t\"state\": kwargs.get(\"state\") or \"Test State\",\n\t\t\t\t\"country\": kwargs.get(\"country\") or \"India\",\n\t\t\t\t\"links\": [\n\t\t\t\t\t{\"link_doctype\": \"Customer\", \"link_name\": kwargs.get(\"customer\") or \"_Test Customer\"}\n\t\t\t\t],\n\t\t\t}\n\t\t).insert()\n\n\tif not frappe.db.get_value(\"Contact\", {\"first_name\": kwargs.get(\"first_name\")}):\n\t\tcontact = frappe.get_doc(\n\t\t\t{\n\t\t\t\t\"doctype\": \"Contact\",\n\t\t\t\t\"first_name\": kwargs.get(\"first_name\"),\n\t\t\t\t\"links\": [\n\t\t\t\t\t{\"link_doctype\": \"Customer\", \"link_name\": kwargs.get(\"customer\") or \"_Test Customer\"}\n\t\t\t\t],\n\t\t\t}\n\t\t)\n\t\tcontact.add_email(kwargs.get(\"email\") or \"test_contact_customer@example.com\", is_primary=True)\n\t\tcontact.add_phone(kwargs.get(\"phone\") or \"+91 0000000000\", is_primary_phone=True)\n\t\tcontact.insert()\n\n\ntest_dependencies = [\n\t\"Sales Taxes and Charges Template\",\n\t\"Price List\",\n\t\"Item Price\",\n\t\"Shipping Rule\",\n\t\"Currency Exchange\",\n\t\"Customer Group\",\n\t\"Lead\",\n\t\"Customer\",\n\t\"Contact\",\n\t\"Address\",\n\t\"Item\",\n\t\"Tax Rule\",\n]"
  },
  {
    "path": "webshop/webshop/shopping_cart/utils.py",
    "content": "# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors\n# License: GNU General Public License v3. See license.txt\nimport frappe\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import is_cart_enabled\n\n\ndef show_cart_count():\n\tif (\n\t\tis_cart_enabled()\n\t\tand frappe.db.get_value(\"User\", frappe.session.user, \"user_type\") == \"Website User\"\n\t):\n\t\treturn True\n\n\treturn False\n\n\ndef set_cart_count(login_manager):\n\t# since this is run only on hooks login event\n\t# make sure user is already a customer\n\t# before trying to set cart count\n\tuser_is_customer = is_customer()\n\tif not user_is_customer:\n\t\treturn\n\n\tif show_cart_count():\n\t\tfrom webshop.webshop.shopping_cart.cart import set_cart_count\n\n\t\t# set_cart_count will try to fetch existing cart quotation\n\t\t# or create one if non existent (and create a customer too)\n\t\t# cart count is calculated from this quotation's items\n\t\tset_cart_count()\n\n\ndef clear_cart_count(login_manager):\n\tif show_cart_count():\n\t\tfrappe.local.cookie_manager.delete_cookie(\"cart_count\")\n\n\ndef update_website_context(context):\n\tcart_enabled = is_cart_enabled()\n\tcontext[\"shopping_cart_enabled\"] = cart_enabled\n\n\ndef is_customer():\n\tif frappe.session.user and frappe.session.user != \"Guest\":\n\t\tcontact_name = frappe.get_value(\"Contact\", {\"email_id\": frappe.session.user})\n\t\tif contact_name:\n\t\t\tcontact = frappe.get_doc(\"Contact\", contact_name)\n\t\t\tfor link in contact.links:\n\t\t\t\tif link.link_doctype == \"Customer\":\n\t\t\t\t\treturn True\n\n\t\treturn False\n"
  },
  {
    "path": "webshop/webshop/utils/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/utils/portal.py",
    "content": "import frappe\nfrom frappe.utils.nestedset import get_root_of\n\nfrom erpnext.portal.utils import create_customer_or_supplier\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n    get_shopping_cart_settings,\n)\nfrom webshop.webshop.shopping_cart.cart import get_debtors_account\n\n\ndef update_debtors_account():\n\tdoc_type = debtors_account = None\n\tuser = frappe.session.user\n\n\tif frappe.db.get_value(\"User\", user, \"user_type\") != \"Website User\":\n\t\treturn\n\n\tuser_roles = frappe.get_roles()\n\tportal_settings = frappe.get_single(\"Portal Settings\")\n\tdefault_role = portal_settings.default_role\n\n\tif default_role not in [\"Customer\", \"Supplier\"]:\n\t\treturn\n\n\tif portal_settings.default_role and portal_settings.default_role in user_roles:\n\t\tdoc_type = portal_settings.default_role\n\n\tif not doc_type:\n\t\treturn\n\n\tif doc_type != \"Customer\":\n\t\treturn\n\n\tif frappe.db.exists(doc_type, user):\n\t\tparty = frappe.get_doc(doc_type, user)\n\telse:\n\t\tparty = create_customer_or_supplier()\n\n\tif not party:\n\t\treturn\n\n\tfullname = frappe.utils.get_fullname(user)\n\tcart_settings = get_shopping_cart_settings()\n\n\tparty.update(\n\t\t{\n\t\t\t\"customer_name\": fullname,\n\t\t\t\"customer_type\": \"Individual\",\n\t\t\t\"customer_group\": cart_settings.default_customer_group,\n\t\t\t\"territory\": get_root_of(\"Territory\"),\n\t\t}\n\t)\n\n\tif cart_settings.enable_checkout:\n\t\tdebtors_account = get_debtors_account(cart_settings)\n\n\tif not debtors_account:\n\t\treturn party\n\n\tparty.update(\n\t\t{\"accounts\": [{\"company\": cart_settings.company, \"account\": debtors_account}]}\n\t)\n\n\treturn party\n"
  },
  {
    "path": "webshop/webshop/utils/product.py",
    "content": "import frappe\nfrom frappe.utils import getdate, nowdate\n\nfrom erpnext.stock.doctype.batch.batch import get_batch_qty\nfrom erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses\n\n\ndef get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None):\n\tin_stock, stock_qty = 0, \"\"\n\ttemplate_item_code, is_stock_item = frappe.db.get_value(\n\t\t\"Item\", item_code, [\"variant_of\", \"is_stock_item\"]\n\t)\n\n\tif not warehouse:\n\t\twarehouse = frappe.db.get_value(\"Website Item\", {\"item_code\": item_code}, item_warehouse_field)\n\n\tif not warehouse and template_item_code and template_item_code != item_code:\n\t\twarehouse = frappe.db.get_value(\n\t\t\t\"Website Item\", {\"item_code\": template_item_code}, item_warehouse_field\n\t\t)\n\n\tif warehouse and frappe.get_cached_value(\"Warehouse\", warehouse, \"is_group\") == 1:\n\t\twarehouses = get_child_warehouses(warehouse)\n\telse:\n\t\twarehouses = [warehouse] if warehouse else []\n\n\ttotal_stock = 0.0\n\tif warehouses:\n\t\tfor warehouse in warehouses:\n\t\t\tstock_qty = frappe.db.sql(\n\t\t\t\t\"\"\"\n\t\t\t\tselect S.actual_qty / IFNULL(C.conversion_factor, 1)\n\t\t\t\tfrom tabBin S\n\t\t\t\tinner join `tabItem` I on S.item_code = I.Item_code\n\t\t\t\tleft join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code\n\t\t\t\twhere S.item_code=%s and S.warehouse=%s\"\"\",\n\t\t\t\t(item_code, warehouse),\n\t\t\t)\n\n\t\t\tif stock_qty:\n\t\t\t\ttotal_stock += adjust_qty_for_expired_items(item_code, stock_qty, warehouse)\n\n\t\tin_stock = total_stock > 0 and 1 or 0\n\n\treturn frappe._dict(\n\t\t{\"in_stock\": in_stock, \"stock_qty\": total_stock, \"is_stock_item\": is_stock_item}\n\t)\n\n\ndef adjust_qty_for_expired_items(item_code, stock_qty, warehouse):\n\tbatches = frappe.get_all(\"Batch\", filters=[{\"item\": item_code}], fields=[\"expiry_date\", \"name\"])\n\texpired_batches = get_expired_batches(batches)\n\tstock_qty = [list(item) for item in stock_qty]\n\n\tfor batch in expired_batches:\n\t\tif warehouse:\n\t\t\tstock_qty[0][0] = max(0, stock_qty[0][0] - get_batch_qty(batch, warehouse))\n\t\telse:\n\t\t\tstock_qty[0][0] = max(0, stock_qty[0][0] - qty_from_all_warehouses(get_batch_qty(batch)))\n\n\t\tif not stock_qty[0][0]:\n\t\t\tbreak\n\n\treturn stock_qty[0][0] if stock_qty else 0\n\n\ndef get_expired_batches(batches):\n\t\"\"\"\n\t:param batches: A list of dict in the form [{'expiry_date': datetime.date(20XX, 1, 1), 'name': 'batch_id'}, ...]\n\t\"\"\"\n\treturn [b.name for b in batches if b.expiry_date and b.expiry_date <= getdate(nowdate())]\n\n\ndef qty_from_all_warehouses(batch_info):\n\t\"\"\"\n\t:param batch_info: A list of dict in the form [{u'warehouse': u'Stores - I', u'qty': 0.8}, ...]\n\t\"\"\"\n\tqty = 0\n\tfor batch in batch_info:\n\t\tqty = qty + batch.qty\n\n\treturn qty\n\n\ndef get_non_stock_item_status(item_code, item_warehouse_field):\n\t# if item is a product bundle, check if its bundle items are in stock\n\tif frappe.db.exists(\"Product Bundle\", item_code):\n\t\titems = frappe.get_doc(\"Product Bundle\", item_code).get_all_children()\n\t\tbundle_warehouse = frappe.db.get_value(\n\t\t\t\"Website Item\", {\"item_code\": item_code}, item_warehouse_field\n\t\t)\n\t\treturn all(\n\t\t\tget_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock\n\t\t\tfor d in items\n\t\t)\n\telse:\n\t\treturn 1\n"
  },
  {
    "path": "webshop/webshop/utils/setup.py",
    "content": "import frappe\n\ndef has_ecommerce_fields() -> bool:\n\ttable = frappe.qb.Table(\"tabSingles\")\n\tquery = (\n\t\tfrappe.qb.from_(table)\n\t\t.select(table.field)\n\t\t.where(table.doctype == \"E Commerce Settings\")\n\t\t.limit(1)\n\t)\n\n\tdata = query.run(as_dict=True)\n\treturn bool(data)"
  },
  {
    "path": "webshop/webshop/variant_selector/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/variant_selector/item_variants_cache.py",
    "content": "import frappe\n\n\nclass ItemVariantsCacheManager:\n\tdef __init__(self, item_code):\n\t\tself.item_code = item_code\n\n\tdef get_item_variants_data(self):\n\t\tval = frappe.cache().hget(\"item_variants_data\", self.item_code)\n\n\t\tif not val:\n\t\t\tself.build_cache()\n\n\t\treturn frappe.cache().hget(\"item_variants_data\", self.item_code)\n\n\tdef get_attribute_value_item_map(self):\n\t\tval = frappe.cache().hget(\"attribute_value_item_map\", self.item_code)\n\n\t\tif not val:\n\t\t\tself.build_cache()\n\n\t\treturn frappe.cache().hget(\"attribute_value_item_map\", self.item_code)\n\n\tdef get_item_attribute_value_map(self):\n\t\tval = frappe.cache().hget(\"item_attribute_value_map\", self.item_code)\n\n\t\tif not val:\n\t\t\tself.build_cache()\n\n\t\treturn frappe.cache().hget(\"item_attribute_value_map\", self.item_code)\n\n\tdef get_optional_attributes(self):\n\t\tval = frappe.cache().hget(\"optional_attributes\", self.item_code)\n\n\t\tif not val:\n\t\t\tself.build_cache()\n\n\t\treturn frappe.cache().hget(\"optional_attributes\", self.item_code)\n\n\tdef get_ordered_attribute_values(self):\n\t\tval = frappe.cache().get_value(\"ordered_attribute_values_map\")\n\t\tif val:\n\t\t\treturn val\n\n\t\tall_attribute_values = frappe.get_all(\n\t\t\t\"Item Attribute Value\", [\"attribute_value\", \"idx\", \"parent\"], order_by=\"idx asc\"\n\t\t)\n\n\t\tordered_attribute_values_map = frappe._dict({})\n\t\tfor d in all_attribute_values:\n\t\t\tordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value)\n\n\t\tfrappe.cache().set_value(\"ordered_attribute_values_map\", ordered_attribute_values_map)\n\t\treturn ordered_attribute_values_map\n\n\tdef build_cache(self):\n\t\tparent_item_code = self.item_code\n\n\t\tattributes = [\n\t\t\ta.attribute\n\t\t\tfor a in frappe.get_all(\n\t\t\t\t\"Item Variant Attribute\", {\"parent\": parent_item_code}, [\"attribute\"], order_by=\"idx asc\"\n\t\t\t)\n\t\t]\n\n\t\t# Get Variants and tehir Attributes that are not disabled\n\t\tiva = frappe.qb.DocType(\"Item Variant Attribute\")\n\t\titem = frappe.qb.DocType(\"Item\")\n\t\tquery = (\n\t\t\tfrappe.qb.from_(iva)\n\t\t\t.join(item)\n\t\t\t.on(item.name == iva.parent)\n\t\t\t.select(iva.parent, iva.attribute, iva.attribute_value)\n\t\t\t.where((iva.variant_of == parent_item_code) & (item.disabled == 0))\n\t\t\t.orderby(iva.name)\n\t\t)\n\t\titem_variants_data = query.run()\n\n\t\tattribute_value_item_map = frappe._dict()\n\t\titem_attribute_value_map = frappe._dict()\n\n\t\tfor row in item_variants_data:\n\t\t\titem_code, attribute, attribute_value = row\n\t\t\t# (attr, value) => [item1, item2]\n\t\t\tattribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code)\n\t\t\t# item => {attr1: value1, attr2: value2}\n\t\t\titem_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value\n\n\t\toptional_attributes = set()\n\t\tfor item_code, attr_dict in item_attribute_value_map.items():\n\t\t\tfor attribute in attributes:\n\t\t\t\tif attribute not in attr_dict:\n\t\t\t\t\toptional_attributes.add(attribute)\n\n\t\tfrappe.cache().hset(\"attribute_value_item_map\", parent_item_code, attribute_value_item_map)\n\t\tfrappe.cache().hset(\"item_attribute_value_map\", parent_item_code, item_attribute_value_map)\n\t\tfrappe.cache().hset(\"item_variants_data\", parent_item_code, item_variants_data)\n\t\tfrappe.cache().hset(\"optional_attributes\", parent_item_code, optional_attributes)\n\n\tdef clear_cache(self):\n\t\tkeys = [\n\t\t\t\"attribute_value_item_map\",\n\t\t\t\"item_attribute_value_map\",\n\t\t\t\"item_variants_data\",\n\t\t\t\"optional_attributes\",\n\t\t]\n\n\t\tfor key in keys:\n\t\t\tfrappe.cache().hdel(key, self.item_code)\n\n\tdef rebuild_cache(self):\n\t\tself.clear_cache()\n\t\tenqueue_build_cache(self.item_code)\n\n\ndef build_cache(item_code):\n\tfrappe.cache().hset(\"item_cache_build_in_progress\", item_code, 1)\n\ti = ItemVariantsCacheManager(item_code)\n\ti.build_cache()\n\tfrappe.cache().hset(\"item_cache_build_in_progress\", item_code, 0)\n\n\ndef enqueue_build_cache(item_code):\n\tif frappe.cache().hget(\"item_cache_build_in_progress\", item_code):\n\t\treturn\n\tfrappe.enqueue(\n\t\t\"webshop.webshop.variant_selector.item_variants_cache.build_cache\",\n\t\titem_code=item_code,\n\t\tqueue=\"long\",\n\t)\n"
  },
  {
    "path": "webshop/webshop/variant_selector/test_variant_selector.py",
    "content": "import frappe\nfrom frappe.tests.utils import FrappeTestCase\n\nfrom erpnext.controllers.item_variant import create_variant\nfrom webshop.webshop.doctype.webshop_settings.test_webshop_settings import (\n\tsetup_webshop_settings,\n)\nfrom webshop.webshop.doctype.website_item.website_item import make_website_item\nfrom webshop.webshop.variant_selector.utils import get_next_attribute_and_values\nfrom erpnext.stock.doctype.item.test_item import make_item\n\ntest_dependencies = [\"Item\"]\n\n\nclass TestVariantSelector(FrappeTestCase):\n\t@classmethod\n\tdef setUpClass(cls):\n\t\tsuper().setUpClass()\n\t\ttemplate_item = make_item(\n\t\t\t\"Test-Tshirt-Temp\",\n\t\t\t{\n\t\t\t\t\"has_variant\": 1,\n\t\t\t\t\"variant_based_on\": \"Item Attribute\",\n\t\t\t\t\"attributes\": [{\"attribute\": \"Test Size\"}, {\"attribute\": \"Test Colour\"}],\n\t\t\t},\n\t\t)\n\n\t\t# create L-R, L-G, M-R, M-G and S-R\n\t\tfor size in (\n\t\t\t\"Large\",\n\t\t\t\"Medium\",\n\t\t):\n\t\t\tfor colour in (\n\t\t\t\t\"Red\",\n\t\t\t\t\"Green\",\n\t\t\t):\n\t\t\t\tvariant = create_variant(\"Test-Tshirt-Temp\", {\"Test Size\": size, \"Test Colour\": colour})\n\t\t\t\tvariant.save()\n\n\t\tvariant = create_variant(\"Test-Tshirt-Temp\", {\"Test Size\": \"Small\", \"Test Colour\": \"Red\"})\n\t\tvariant.save()\n\n\t\tmake_website_item(template_item)  # publish template not variants\n\n\tdef test_item_attributes(self):\n\t\t\"\"\"\n\t\tTest if the right attributes are fetched in the popup.\n\t\t(Attributes must only come from active items)\n\n\t\tAttribute selection must not be linked to Website Items.\n\t\t\"\"\"\n\t\tfrom webshop.webshop.variant_selector.utils import get_attributes_and_values\n\n\t\tattr_data = get_attributes_and_values(\"Test-Tshirt-Temp\")\n\n\t\tself.assertEqual(attr_data[0][\"attribute\"], \"Test Size\")\n\t\tself.assertEqual(attr_data[1][\"attribute\"], \"Test Colour\")\n\t\tself.assertEqual(len(attr_data[0][\"values\"]), 3)  # ['Small', 'Medium', 'Large']\n\t\tself.assertEqual(len(attr_data[1][\"values\"]), 2)  # ['Red', 'Green']\n\n\t\t# disable small red tshirt, now there are no small tshirts.\n\t\t# but there are some red tshirts\n\t\tsmall_variant = frappe.get_doc(\"Item\", \"Test-Tshirt-Temp-S-R\")\n\t\tsmall_variant.disabled = 1\n\t\tsmall_variant.save()  # trigger cache rebuild\n\n\t\tattr_data = get_attributes_and_values(\"Test-Tshirt-Temp\")\n\n\t\t# Only L and M attribute values must be fetched since S is disabled\n\t\tself.assertEqual(len(attr_data[0][\"values\"]), 2)  # ['Medium', 'Large']\n\n\t\t# teardown\n\t\tsmall_variant.disabled = 0\n\t\tsmall_variant.save()\n\n\tdef test_next_item_variant_values(self):\n\t\t\"\"\"\n\t\tTest if on selecting an attribute value, the next possible values\n\t\tare filtered accordingly.\n\t\tValues that dont apply should not be fetched.\n\t\tE.g.\n\t\tThere is a ** Small-Red ** Tshirt. No other colour in this size.\n\t\tOn selecting ** Small **, only ** Red ** should be selectable next.\n\t\t\"\"\"\n\t\tnext_values = get_next_attribute_and_values(\n\t\t\t\"Test-Tshirt-Temp\", selected_attributes={\"Test Size\": \"Small\"}\n\t\t)\n\t\tnext_colours = next_values[\"valid_options_for_attributes\"][\"Test Colour\"]\n\t\tfiltered_items = next_values[\"filtered_items\"]\n\n\t\tself.assertEqual(len(next_colours), 1)\n\t\tself.assertEqual(next_colours.pop(), \"Red\")\n\t\tself.assertEqual(len(filtered_items), 1)\n\t\tself.assertEqual(filtered_items.pop(), \"Test-Tshirt-Temp-S-R\")\n\n\tdef test_exact_match_with_price(self):\n\t\t\"\"\"\n\t\tTest price fetching and matching of variant without Website Item\n\t\t\"\"\"\n\t\tfrom webshop.webshop.doctype.website_item.test_website_item import make_web_item_price\n\n\t\tfrappe.set_user(\"Administrator\")\n\t\tsetup_webshop_settings(\n\t\t\t{\n\t\t\t\t\"company\": \"_Test Company\",\n\t\t\t\t\"enabled\": 1,\n\t\t\t\t\"default_customer_group\": \"_Test Customer Group\",\n\t\t\t\t\"price_list\": \"_Test Price List India\",\n\t\t\t\t\"show_price\": 1,\n\t\t\t}\n\t\t)\n\n\t\tmake_web_item_price(item_code=\"Test-Tshirt-Temp-S-R\", price_list_rate=100)\n\n\t\tfrappe.local.shopping_cart_settings = None  # clear cached settings values\n\t\tnext_values = get_next_attribute_and_values(\n\t\t\t\"Test-Tshirt-Temp\", selected_attributes={\"Test Size\": \"Small\", \"Test Colour\": \"Red\"}\n\t\t)\n\t\tprint(\">>>>\", next_values)\n\t\tprice_info = next_values[\"product_info\"][\"price\"]\n\n\t\tself.assertEqual(next_values[\"exact_match\"][0], \"Test-Tshirt-Temp-S-R\")\n\t\tself.assertEqual(next_values[\"exact_match\"][0], \"Test-Tshirt-Temp-S-R\")\n\t\tself.assertEqual(price_info[\"price_list_rate\"], 100.0)\n\t\tself.assertEqual(price_info[\"formatted_price_sales_uom\"], \"₹ 100.00\")\n"
  },
  {
    "path": "webshop/webshop/variant_selector/utils.py",
    "content": "import frappe\nfrom frappe.utils import cint, flt\n\nfrom webshop.webshop.doctype.webshop_settings.webshop_settings import (\n\tget_shopping_cart_settings,\n)\nfrom webshop.webshop.shopping_cart.cart import _set_price_list\nfrom webshop.webshop.variant_selector.item_variants_cache import ItemVariantsCacheManager\nfrom erpnext.utilities.product import get_price\n\n\ndef get_item_codes_by_attributes(attribute_filters, template_item_code=None):\n\titems = []\n\n\tfor attribute, values in attribute_filters.items():\n\t\tattribute_values = values\n\n\t\tif not isinstance(attribute_values, list):\n\t\t\tattribute_values = [attribute_values]\n\n\t\tif not attribute_values:\n\t\t\tcontinue\n\n\t\twheres = []\n\t\tquery_values = []\n\t\tfor attribute_value in attribute_values:\n\t\t\twheres.append(\"( attribute = %s and attribute_value = %s )\")\n\t\t\tquery_values += [attribute, attribute_value]\n\n\t\tattribute_query = \" or \".join(wheres)\n\n\t\tif template_item_code:\n\t\t\tvariant_of_query = \"AND t2.variant_of = %s\"\n\t\t\tquery_values.append(template_item_code)\n\t\telse:\n\t\t\tvariant_of_query = \"\"\n\n\t\tquery = \"\"\"\n\t\t\tSELECT\n\t\t\t\tt1.parent\n\t\t\tFROM\n\t\t\t\t`tabItem Variant Attribute` t1\n\t\t\tWHERE\n\t\t\t\t1 = 1\n\t\t\t\tAND (\n\t\t\t\t\t{attribute_query}\n\t\t\t\t)\n\t\t\t\tAND EXISTS (\n\t\t\t\t\tSELECT\n\t\t\t\t\t\t1\n\t\t\t\t\tFROM\n\t\t\t\t\t\t`tabItem` t2\n\t\t\t\t\tWHERE\n\t\t\t\t\t\tt2.name = t1.parent\n\t\t\t\t\t\t{variant_of_query}\n\t\t\t\t)\n\t\t\tGROUP BY\n\t\t\t\tt1.parent\n\t\t\tORDER BY\n\t\t\t\tNULL\n\t\t\"\"\".format(\n\t\t\tattribute_query=attribute_query, variant_of_query=variant_of_query\n\t\t)\n\n\t\titem_codes = set([r[0] for r in frappe.db.sql(query, query_values)])  # nosemgrep\n\t\titems.append(item_codes)\n\n\tres = list(set.intersection(*items))\n\n\treturn res\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_attributes_and_values(item_code):\n\t\"\"\"Build a list of attributes and their possible values.\n\tThis will ignore the values upon selection of which there cannot exist one item.\n\t\"\"\"\n\titem_cache = ItemVariantsCacheManager(item_code)\n\titem_variants_data = item_cache.get_item_variants_data()\n\n\tattributes = get_item_attributes(item_code)\n\tattribute_list = [a.attribute for a in attributes]\n\n\tvalid_options = {}\n\tfor item_code, attribute, attribute_value in item_variants_data:\n\t\tif attribute in attribute_list:\n\t\t\tvalid_options.setdefault(attribute, set()).add(attribute_value)\n\n\titem_attribute_values = frappe.db.get_all(\n\t\t\"Item Attribute Value\", [\"parent\", \"attribute_value\", \"idx\"], order_by=\"parent asc, idx asc\"\n\t)\n\tordered_attribute_value_map = frappe._dict()\n\tfor iv in item_attribute_values:\n\t\tordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)\n\n\t\"\"\"Numeric attributes are not stored in the Item Attribute Value table.\n\tHowever, they are included in valid_options. If they are not found in ordered_attribute_value_map\n\tsort and add them to it. This does not include the entire range if there is no\n\tproduct associated with specific number. Only possible values are returned.\n\t\"\"\"\n\tfor attr_name in attribute_list:\n\t\tif attr_name not in ordered_attribute_value_map:\n\t\t\tnumeric_list = sorted([i for i in valid_options[attr_name] if i.replace(\".\",\"\").isnumeric()], key=float)\n\t\t\tordered_attribute_value_map[attr_name] = numeric_list\n\n\t# build attribute values in idx order\n\tfor attr in attributes:\n\t\tvalid_attribute_values = valid_options.get(attr.attribute, [])\n\t\tordered_values = ordered_attribute_value_map.get(attr.attribute, [])\n\t\tattr[\"values\"] = [v for v in ordered_values if v in valid_attribute_values]\n\n\treturn attributes\n\n\n@frappe.whitelist(allow_guest=True)\ndef get_next_attribute_and_values(item_code, selected_attributes):\n\tfrom erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses\n\n\t\"\"\"Find the count of Items that match the selected attributes.\n\tAlso, find the attribute values that are not applicable for further searching.\n\tIf less than equal to 10 items are found, return item_codes of those items.\n\tIf one item is matched exactly, return item_code of that item.\n\t\"\"\"\n\tselected_attributes = frappe.parse_json(selected_attributes)\n\n\titem_cache = ItemVariantsCacheManager(item_code)\n\titem_variants_data = item_cache.get_item_variants_data()\n\n\tattributes = get_item_attributes(item_code)\n\tattribute_list = [a.attribute for a in attributes]\n\tfiltered_items = get_items_with_selected_attributes(item_code, selected_attributes)\n\n\tnext_attribute = None\n\n\tfor attribute in attribute_list:\n\t\tif attribute not in selected_attributes:\n\t\t\tnext_attribute = attribute\n\t\t\tbreak\n\n\tvalid_options_for_attributes = frappe._dict()\n\n\tfor a in attribute_list:\n\t\tvalid_options_for_attributes[a] = set()\n\n\t\tselected_attribute = selected_attributes.get(a, None)\n\t\tif selected_attribute:\n\t\t\t# already selected attribute values are valid options\n\t\t\tvalid_options_for_attributes[a].add(selected_attribute)\n\n\tfor row in item_variants_data:\n\t\titem_code, attribute, attribute_value = row\n\t\tif (\n\t\t\titem_code in filtered_items\n\t\t\tand attribute not in selected_attributes\n\t\t\tand attribute in attribute_list\n\t\t):\n\t\t\tvalid_options_for_attributes[attribute].add(attribute_value)\n\n\toptional_attributes = item_cache.get_optional_attributes()\n\texact_match = []\n\t# search for exact match if all selected attributes are required attributes\n\tif len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):\n\t\titem_attribute_value_map = item_cache.get_item_attribute_value_map()\n\t\tfor item_code, attr_dict in item_attribute_value_map.items():\n\t\t\tif item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):\n\t\t\t\texact_match.append(item_code)\n\n\tfiltered_items_count = len(filtered_items)\n\n\tif exact_match:\n\t\tcart_settings = get_shopping_cart_settings()\n\t\tproduct_info = get_item_variant_price_dict(exact_match[0], cart_settings)\n\n\t\tif product_info:\n\t\t\tproduct_info[\"is_stock_item\"] = frappe.get_cached_value(\"Item\", exact_match[0], \"is_stock_item\")\n\t\t\tproduct_info[\"allow_items_not_in_stock\"] = cint(cart_settings.allow_items_not_in_stock)\n\telse:\n\t\tproduct_info = None\n\n\tproduct_id = \"\"\n\twarehouse = \"\"\n\tif exact_match or filtered_items:\n\t\tif exact_match and len(exact_match) == 1:\n\t\t\tproduct_id = exact_match[0]\n\t\telif filtered_items_count == 1:\n\t\t\tproduct_id = list(filtered_items)[0]\n\n\tif product_id:\n\t\twarehouse = frappe.get_cached_value(\n\t\t\t\"Website Item\", {\"item_code\": product_id}, \"website_warehouse\"\n\t\t)\n\n\tavailable_qty = 0.0\n\tif warehouse and frappe.get_cached_value(\"Warehouse\", warehouse, \"is_group\") == 1:\n\t\twarehouses = get_child_warehouses(warehouse)\n\telse:\n\t\twarehouses = [warehouse] if warehouse else []\n\n\tfor warehouse in warehouses:\n\t\tavailable_qty += flt(\n\t\t\tfrappe.db.get_value(\"Bin\", {\"item_code\": product_id, \"warehouse\": warehouse}, \"actual_qty\")\n\t\t)\n\n\treturn {\n\t\t\"next_attribute\": next_attribute,\n\t\t\"valid_options_for_attributes\": valid_options_for_attributes,\n\t\t\"filtered_items_count\": filtered_items_count,\n\t\t\"filtered_items\": filtered_items if filtered_items_count < 10 else [],\n\t\t\"exact_match\": exact_match,\n\t\t\"product_info\": product_info,\n\t\t\"available_qty\": available_qty,\n\t}\n\n\ndef get_items_with_selected_attributes(item_code, selected_attributes):\n\titem_cache = ItemVariantsCacheManager(item_code)\n\tattribute_value_item_map = item_cache.get_attribute_value_item_map()\n\n\titems = []\n\tfor attribute, value in selected_attributes.items():\n\t\tfiltered_items = attribute_value_item_map.get((attribute, value), [])\n\t\titems.append(set(filtered_items))\n\n\treturn set.intersection(*items)\n\n\n# utilities\n\n\ndef get_item_attributes(item_code):\n\tattributes = frappe.db.get_all(\n\t\t\"Item Variant Attribute\",\n\t\tfields=[\"attribute\"],\n\t\tfilters={\"parenttype\": \"Item\", \"parent\": item_code},\n\t\torder_by=\"idx asc\",\n\t)\n\n\toptional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()\n\n\tfor a in attributes:\n\t\tif a.attribute in optional_attributes:\n\t\t\ta.optional = True\n\n\treturn attributes\n\n\ndef get_item_variant_price_dict(item_code, cart_settings):\n\tif cart_settings.enabled and cart_settings.show_price:\n\t\tis_guest = frappe.session.user == \"Guest\"\n\t\t# Show Price if logged in.\n\t\t# If not logged in, check if price is hidden for guest.\n\t\tif not is_guest or not cart_settings.hide_price_for_guest:\n\t\t\tprice_list = _set_price_list(cart_settings, None)\n\t\t\tprice = get_price(\n\t\t\t\titem_code, price_list, cart_settings.default_customer_group, cart_settings.company\n\t\t\t)\n\t\t\treturn {\"price\": price}\n\n\treturn None\n"
  },
  {
    "path": "webshop/webshop/web_template/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/web_template/hero_slider/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/web_template/hero_slider/hero_slider.html",
    "content": "{%- macro slide(image, title, subtitle, action, label, index, align=\"Left\", theme=\"Dark\") -%}\n{%- set align_class = resolve_class({\n\t'text-right': align == 'Right',\n\t'text-centre': align == 'Centre',\n\t'text-left': align == 'Left',\n}) -%}\n\n{%- set heading_class = resolve_class({\n\t'text-white': theme == 'Dark',\n\t'': theme == 'Light',\n}) -%}\n<div class=\"carousel-item {{ 'active' if index=='1' else ''}}\" style=\"height: 450px;\">\n\t<img class=\"d-block h-100 w-100\" style=\"object-fit: cover;\" src=\"{{ image }}\" alt=\"{{ _(title) }}\">\n\t{%- if title or subtitle -%}\n\t<div class=\"carousel-body container d-flex {{ align_class }}\">\n\t\t<div class=\"carousel-content align-self-center\">\n\t\t\t{%- if title -%}<h1 class=\"{{ heading_class }}\">{{ _(title) }}</h1>{%- endif -%}\n\t\t\t{%- if subtitle -%}<p class=\"{{ heading_class }} mt-2\">{{ _(subtitle) }}</p>{%- endif -%}\n\t\t\t{%- if action -%}\n\t\t\t<a href=\"{{ action }}\" class=\"btn btn-primary mt-3\">\n\t\t\t\t{{ _(label) }}\n\t\t\t</a>\n\t\t\t{%- endif -%}\n\t\t</div>\n\t</div>\n\t{%- endif -%}\n</div>\n{%- endmacro -%}\n\n{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%}\n\n<div id=\"{{ hero_slider_id }}\" class=\"section-carousel carousel slide\" data-ride=\"carousel\">\n\t{%- if show_indicators -%}\n\t<ol class=\"carousel-indicators\">\n\t\t{%- for index in ['1', '2', '3', '4', '5'] -%}\n\t\t{%- if values['slide_' + index + '_image'] -%}\n\t\t\t<li data-target=\"#{{ hero_slider_id }}\" data-slide-to=\"{{ frappe.utils.cint(index) - 1 }}\" class=\"{{ 'active' if index=='1' else ''}}\"></li>\n\t\t{%- endif -%}\n\t\t{%- endfor -%}\n\t</ol>\n\t{%- endif -%}\n\t<div class=\"carousel-inner {{ resolve_class({'rounded-carousel': rounded }) }}\">\n\t\t{%- for index in ['1', '2', '3', '4', '5'] -%}\n\t\t\t{%- set image = values['slide_' + index + '_image'] -%}\n\t\t\t{%- set title = values['slide_' + index + '_title'] -%}\n\t\t\t{%- set subtitle = values['slide_' + index + '_subtitle'] -%}\n\t\t\t{%- set primary_action = values['slide_' + index + '_primary_action'] -%}\n\t\t\t{%- set primary_action_label = values['slide_' + index + '_primary_action_label'] -%}\n\t\t\t{%- set align = values['slide_' + index + '_content_align'] -%}\n\t\t\t{%- set theme = values['slide_' + index + '_theme'] -%}\n\n\t\t\t{%- if image -%}\n\t\t\t\t{{ slide(image, title, subtitle, primary_action, primary_action_label, index, align, theme) }}\n\t\t\t{%- endif -%}\n\n\t\t{%- endfor -%}\n\t</div>\n\t{%- if show_controls -%}\n\t<a class=\"carousel-control-prev\" href=\"#{{ hero_slider_id }}\" role=\"button\" data-slide=\"prev\">\n\t\t<div class=\"carousel-control\">\n\t\t\t<svg class=\"mr-1\" width=\"20\" height=\"20\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n\t\t\t\t<path d=\"M11.625 3.75L6.375 9L11.625 14.25\" stroke=\"#4C5A67\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t</svg>\n\t\t</div>\n\t\t<span class=\"sr-only\">{{ _(\"Previous\") }}</span>\n\t</a>\n\t<a class=\"carousel-control-next\" href=\"#{{ hero_slider_id }}\" role=\"button\" data-slide=\"next\">\n\t\t<div class=\"carousel-control\">\n\t\t\t<svg class=\"ml-1\" width=\"20\" height=\"20\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n\t\t\t\t<path d=\"M6.375 14.25L11.625 9L6.375 3.75\" stroke=\"#4C5A67\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\t\t\t</svg>\n\t\t</div>\n\t\t<span class=\"sr-only\">{{ _(\"Next\") }}</span>\n\t</a>\n\t{%- endif -%}\n</div>\n\n<script>\n\tfrappe.ready(function () {\n\t\t$('.carousel').carousel({\n\t\t\tinterval: false,\n\t\t\tpause: \"hover\",\n\t\t\twrap: true\n\t\t})\n\t});\n</script>\n"
  },
  {
    "path": "webshop/webshop/web_template/hero_slider/hero_slider.json",
    "content": "{\n \"__unsaved\": 1,\n \"creation\": \"2020-11-17 15:21:51.207221\",\n \"docstatus\": 0,\n \"doctype\": \"Web Template\",\n \"fields\": [\n  {\n   \"fieldname\": \"slider_name\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Slider Name\",\n   \"reqd\": 1\n  },\n  {\n   \"default\": \"1\",\n   \"fieldname\": \"show_indicators\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Indicators\",\n   \"reqd\": 0\n  },\n  {\n   \"default\": \"1\",\n   \"fieldname\": \"show_controls\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Show Controls\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Slide 1\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_image\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Image\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_subtitle\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_primary_action_label\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action Label\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_primary_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_content_align\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Content Align\",\n   \"options\": \"Left\\nCentre\\nRight\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_1_theme\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Slide Theme\",\n   \"options\": \"Dark\\nLight\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Slide 2\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2_image\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Image\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2_title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2_subtitle\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2_primary_action_label\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action Label\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2_primary_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action\",\n   \"reqd\": 0\n  },\n  {\n   \"default\": \"Left\",\n   \"fieldname\": \"slide_2_content_align\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Content Align\",\n   \"options\": \"Left\\nCentre\\nRight\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_2_theme\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Slide Theme\",\n   \"options\": \"Dark\\nLight\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Slide 3\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_image\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Image\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_subtitle\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_primary_action_label\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action Label\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_primary_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_content_align\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Content Align\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_3_theme\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Slide Theme\",\n   \"options\": \"Dark\\nLight\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Slide 4\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_image\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Image\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_subtitle\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_primary_action_label\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action Label\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_primary_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_content_align\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Content Align\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_4_theme\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Slide Theme\",\n   \"options\": \"Dark\\nLight\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Slide 5\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_image\",\n   \"fieldtype\": \"Attach Image\",\n   \"label\": \"Image\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_subtitle\",\n   \"fieldtype\": \"Small Text\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_primary_action_label\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action Label\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_primary_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_content_align\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Content Align\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"slide_5_theme\",\n   \"fieldtype\": \"Select\",\n   \"label\": \"Slide Theme\",\n   \"options\": \"Dark\\nLight\",\n   \"reqd\": 0\n  }\n ],\n \"idx\": 2,\n \"modified\": \"2023-10-16 18:00:52.933687\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Hero Slider\",\n \"owner\": \"Administrator\",\n \"standard\": 1,\n \"template\": \"\",\n \"type\": \"Section\"\n}\n"
  },
  {
    "path": "webshop/webshop/web_template/item_card_group/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/web_template/item_card_group/item_card_group.html",
    "content": "{% from \"webshop/templates/includes/macros.html\" import item_card, item_card_body %}\n\n<div class=\"section-with-cards item-card-group-section\">\n\t<div class=\"item-group-header d-flex justify-content-between\">\n\t\t<div class=\"title-section\">\n\t\t\t{%- if title -%}\n\t\t\t<h2 class=\"section-title\">{{ title }}</h2>\n\t\t\t{%- endif -%}\n\t\t\t{%- if subtitle -%}\n\t\t\t<p class=\"section-description\">{{ subtitle }}</p>\n\t\t\t{%- endif -%}\n\t\t</div>\n\t\t<div class=\"primary-action-section\">\n\t\t\t{%- if primary_action -%}\n\t\t\t<a href=\"{{ action }}\" class=\"btn btn-primary pull-right\">\n\t\t\t\t{{ primary_action_label }}\n\t\t\t</a>\n\t\t\t{%- endif -%}\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}\n\t\t{%- set item = values['card_' + index + '_item'] -%}\n\t\t\t{%- if item -%}\n\t\t\t\t{%- set web_item = frappe.get_doc(\"Website Item\", item) -%}\n\t\t\t\t{{ item_card(\n\t\t\t\t\tweb_item, is_featured=values['card_' + index + '_featured'],\n\t\t\t\t\tis_full_width=True, align=\"Center\"\n\t\t\t\t) }}\n\t\t\t{%- endif -%}\n\t\t{%- endfor -%}\n\t</div>\n</div>\n\n<style>\n</style>\n"
  },
  {
    "path": "webshop/webshop/web_template/item_card_group/item_card_group.json",
    "content": "{\n \"__unsaved\": 1,\n \"creation\": \"2020-11-17 15:35:05.285322\",\n \"docstatus\": 0,\n \"doctype\": \"Web Template\",\n \"fields\": [\n  {\n   \"fieldname\": \"title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 1\n  },\n  {\n   \"fieldname\": \"subtitle\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"primary_action_label\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action Label\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"primary_action\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Primary Action\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_1\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 1\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_1_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_1_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_2\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 2\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_2_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_2_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_3\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 3\",\n   \"options\": \"\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_3_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_3_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_4\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 4\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_4_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_4_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_5\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 5\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_5_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_5_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_6\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 6\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_6_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_6_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_7\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 7\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_7_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_7_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_8\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 8\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_8_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_8_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_9\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 9\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_9_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_9_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_10\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 10\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_10_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_10_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_11\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 11\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_11_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_11_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_12\",\n   \"fieldtype\": \"Section Break\",\n   \"label\": \"Card 12\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_12_item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Website Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"card_12_featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"reqd\": 0\n  }\n ],\n \"idx\": 0,\n \"modified\": \"2023-10-16 18:01:47.576190\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Item Card Group\",\n \"owner\": \"Administrator\",\n \"standard\": 1,\n \"template\": \"\",\n \"type\": \"Section\"\n}"
  },
  {
    "path": "webshop/webshop/web_template/product_card/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/web_template/product_card/product_card.html",
    "content": "{%- from \"webshop/templates/includes/macros.html\" import item_card -%}\n\n<div class=\"section-with-cards item-card-group-section\">\n    <div class=\"container\">\n        <div class=\"row\">\n            {%- set item_doc = frappe.get_doc(\"Website Item\", values[\"item\"]) -%}\n            {{ item_card(item=item_doc, is_featured=values[\"featured\"], is_full_width=True, align=\"Center\", template=\"Product Card\") }}\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "webshop/webshop/web_template/product_card/product_card.json",
    "content": "{\n \"__unsaved\": 1,\n \"creation\": \"2020-11-17 15:28:47.809342\",\n \"docstatus\": 0,\n \"doctype\": \"Web Template\",\n \"fields\": [\n  {\n   \"fieldname\": \"item\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item\",\n   \"options\": \"Website Item\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"featured\",\n   \"fieldtype\": \"Check\",\n   \"label\": \"Featured\",\n   \"options\": \"\",\n   \"reqd\": 0\n  }\n ],\n \"idx\": 0,\n \"modified\": \"2024-06-17 15:15:09.902754\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Product Card\",\n \"owner\": \"Administrator\",\n \"standard\": 1,\n \"template\": \"\",\n \"type\": \"Component\"\n}"
  },
  {
    "path": "webshop/webshop/web_template/product_category_cards/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/webshop/web_template/product_category_cards/product_category_cards.html",
    "content": "{%- macro card(title, image, url, text_primary=False) -%}\n{%- set align_class = resolve_class({\n\t'text-right': text_primary,\n\t'text-centre': align == 'Center',\n\t'text-left': align == 'Left',\n}) -%}\n<div class=\"card h-100\">\n\t{% if image %}\n\t<img class=\"card-img-top\" src=\"{{ image }}\" alt=\"{{ title }}\" style=\"max-height: 200px;\">\n\t{% else %}\n\t<div class=\"placeholder-div\" style=\"max-height: 200px;\">\n\t\t<span class=\"placeholder\">\n\t\t\t{{ frappe.utils.get_abbr(title or '') }}\n\t\t</span>\n\t</div>\n\t{% endif %}\n\n\t<div class=\"card-body text-center text-muted small\">\n\t\t{{ title or '' }}\n\t</div>\n\t<a href=\"{{ url or '#' }}\" class=\"stretched-link\"></a>\n</div>\n{%- endmacro -%}\n\n<div class=\"section-with-cards product-category-section\">\n\t{%- if title -%}\n\t<h2 class=\"section-title\">{{ title }}</h2>\n\t{%- endif -%}\n\t{%- if subtitle -%}\n\t<p class=\"section-description\">{{ subtitle }}</p>\n\t{%- endif -%}\n\t<!-- {%- set card_size = card_size or 'Small' -%} -->\n\t<div class=\"{{ resolve_class({'mt-6': title}) }}\">\n\t\t<div class=\"card-grid\">\n\t\t\t{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%}\n\t\t\t{%- set category = values['category_' + index] -%}\n\t\t\t\t{%- if category -%}\n\t\t\t\t\t{%- set category = frappe.get_doc(\"Item Group\", category) -%}\n\t\t\t\t\t{{ card(category.name, category.image, category.route) }}\n\t\t\t\t{%- endif -%}\n\t\t\t{%- endfor -%}\n\t\t</div>\n\t</div>\n</div>\n\n<style>\n</style>\n"
  },
  {
    "path": "webshop/webshop/web_template/product_category_cards/product_category_cards.json",
    "content": "{\n \"__unsaved\": 1,\n \"creation\": \"2020-11-17 15:25:50.855934\",\n \"docstatus\": 0,\n \"doctype\": \"Web Template\",\n \"fields\": [\n  {\n   \"fieldname\": \"title\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Title\",\n   \"reqd\": 1\n  },\n  {\n   \"fieldname\": \"subtitle\",\n   \"fieldtype\": \"Data\",\n   \"label\": \"Subtitle\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_1\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_2\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_3\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_4\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_5\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_6\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_7\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  },\n  {\n   \"fieldname\": \"category_8\",\n   \"fieldtype\": \"Link\",\n   \"label\": \"Item Group\",\n   \"options\": \"Item Group\",\n   \"reqd\": 0\n  }\n ],\n \"idx\": 0,\n \"modified\": \"2023-10-16 18:02:24.342186\",\n \"modified_by\": \"Administrator\",\n \"module\": \"Webshop\",\n \"name\": \"Product Category Cards\",\n \"owner\": \"Administrator\",\n \"standard\": 1,\n \"template\": \"\",\n \"type\": \"Section\"\n}"
  },
  {
    "path": "webshop/www/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/www/all-products/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/www/all-products/index.html",
    "content": "{% from \"webshop/templates/includes/macros.html\" import attribute_filter_section, field_filter_section, discount_range_filters %}\n{% extends \"templates/web.html\" %}\n\n{% block title %}{{ _(\"All Products\") }}{% endblock %}\n{% block header %}\n<div class=\"mb-6\">{{ _(\"All Products\") }}</div>\n{% endblock header %}\n\n{% block page_content %}\n<div class=\"row\">\n\t<!-- Items section -->\n\t<div id=\"product-listing\" class=\"col-12 order-2 col-md-9 order-md-2 item-card-group-section\">\n\t\t<!-- Rendered via JS -->\n\t</div>\n\n\t<!-- Filters Section -->\n\t<div class=\"col-12 order-1 col-md-3 order-md-1\">\n\t\t<div class=\"collapse d-md-block mr-4 filters-section\" id=\"product-filters\">\n\t\t\t<div class=\"d-flex justify-content-between align-items-center mb-5 title-section\">\n\t\t\t\t<div class=\"mb-4 filters-title\" > {{ _('Filters') }} </div>\n\t\t\t\t<a class=\"mb-4 clear-filters\" href=\"/all-products\">{{ _('Clear All') }}</a>\n\t\t\t</div>\n\t\t\t<!-- field filters -->\n\t\t\t{% if field_filters %}\n\t\t\t\t{{ _(field_filter_section(field_filters)) }}\n\t\t\t{% endif %}\n\n\t\t\t<!-- attribute filters -->\n\t\t\t{% if attribute_filters %}\n\t\t\t\t{{ _(attribute_filter_section(attribute_filters)) }}\n\t\t\t{% endif %}\n\t\t</div>\n\n\t</div>\n</div>\n\n<script>\n\tfrappe.ready(() => {\n\t\t$('.btn-prev, .btn-next').click((e) => {\n\t\t\tconst $btn = $(e.target);\n\t\t\t$btn.prop('disabled', true);\n\t\t\tconst start = $btn.data('start');\n\t\t\tlet query_params = frappe.utils.get_query_params();\n\t\t\tquery_params.start = start;\n\t\t\tlet path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);\n\t\t\twindow.location.href = path;\n\t\t});\n\t});\n</script>\n\n{% endblock %}\n"
  },
  {
    "path": "webshop/www/all-products/index.js",
    "content": "$(() => {\n\tclass ProductListing {\n\t\tconstructor() {\n\t\t\tlet me = this;\n\t\t\tlet is_item_group_page = $(\".item-group-content\").data(\"item-group\");\n\t\t\tthis.item_group = is_item_group_page || null;\n\n\t\t\tlet view_type = localStorage.getItem(\"product_view\") || \"List View\";\n\n\t\t\t// Render Product Views, Filters & Search\n\t\t\tnew webshop.ProductView({\n\t\t\t\tview_type: view_type,\n\t\t\t\tproducts_section: $('#product-listing'),\n\t\t\t\titem_group: me.item_group\n\t\t\t});\n\n\t\t\tthis.bind_card_actions();\n\t\t}\n\n\t\tbind_card_actions() {\n\t\t\twebshop.webshop.shopping_cart.bind_add_to_cart_action();\n\t\t\twebshop.webshop.wishlist.bind_wishlist_action();\n\t\t}\n\t}\n\n\tnew ProductListing();\n});\n"
  },
  {
    "path": "webshop/www/all-products/index.py",
    "content": "import frappe\nfrom frappe.utils import cint\n\nfrom webshop.webshop.product_data_engine.filters import ProductFiltersBuilder\n\nsitemap = 1\n\n\ndef get_context(context):\n\t# Add homepage as parent\n\tcontext.body_class = \"product-page\"\n\tcontext.parents = [{\"name\": frappe._(\"Home\"), \"route\": \"/\"}]\n\n\tfilter_engine = ProductFiltersBuilder()\n\tcontext.field_filters = filter_engine.get_field_filters()\n\tcontext.attribute_filters = filter_engine.get_attribute_filters()\n\n\tcontext.page_length = (\n\t\tcint(frappe.db.get_single_value(\"Webshop Settings\", \"products_per_page\")) or 20\n\t)\n\n\tcontext.no_cache = 1\n"
  },
  {
    "path": "webshop/www/all-products/not_found.html",
    "content": "<div class=\"d-flex justify-content-center p-3 text-muted\">{{ _('No products found') }}</div>\n"
  },
  {
    "path": "webshop/www/shop-by-category/__init__.py",
    "content": ""
  },
  {
    "path": "webshop/www/shop-by-category/category_card_section.html",
    "content": "{%- macro card(title, image, type, url=None, text_primary=False) -%}\n<!-- style defined at shop-by-category index -->\n<div class=\"card category-card\" data-type=\"{{ type }}\" data-name=\"{{ title }}\">\n\t{% if image %}\n\t<img class=\"card-img-top\" src=\"{{ image }}\" alt=\"{{ title }}\" style=\"height: 80%;\">\n\t{% else %}\n\t<div class=\"placeholder-div\">\n\t\t<span class=\"placeholder\">\n\t\t\t{{ frappe.utils.get_abbr(title) }}\n\t\t</span>\n\t</div>\n\t{% endif %}\n\t<div class=\"card-body text-center text-muted\">\n\t\t{{ title or '' }}\n\t</div>\n\t<a href=\"{{ url or '#' }}\" class=\"stretched-link\"></a>\n</div>\n{%- endmacro -%}\n\n<div class=\"col-12 item-card-group-section\">\n\t<div class=\"row products-list product-category-section\">\n\t\t{%- for row in data -%}\n\t\t\t{%- set title = row.name -%}\n\t\t\t{%- set image = row.get(\"image\") -%}\n\t\t\t{%- if title -%}\n\t\t\t\t{{ card(title, image, type, row.get(\"route\")) }}\n\t\t\t{%- endif -%}\n\t\t{%- endfor -%}\n\t</div>\n</div>"
  },
  {
    "path": "webshop/www/shop-by-category/index.html",
    "content": "{% extends \"templates/web.html\" %}\n{% block title %}{{ _('Shop by Category') }}{% endblock %}\n\n{% block head_include %}\n<style>\n\t.category-slideshow {\n\t\tmargin-bottom: 2rem;\n\t}\n\t.category-card {\n\t\theight: 300px !important;\n\t\twidth: 300px !important;\n\t\tmargin: 30px !important;\n\t}\n</style>\n{% endblock %}\n\n{% block script %}\n<script type=\"text/javascript\" src=\"/shop-by-category/index.js\"></script>\n{% endblock %}\n\n{% block page_content %}\n<div class=\"shop-by-category-content\">\n\t<div class=\"category-slideshow\">\n\t\t{% if slideshow %}\n\t\t<!-- slideshow -->\n\t\t\t{{ web_block(\n\t\t\t\t\"Hero Slider\",\n\t\t\t\tvalues=slideshow,\n\t\t\t\tadd_container=0,\n\t\t\t\tadd_top_padding=0,\n\t\t\t\tadd_bottom_padding=0,\n\t\t\t) }}\n\t\t{% endif %}\n\t</div>\n\t<div class=\"category-tabs\">\n\t\t{% if tabs %}\n\t\t<!-- tabs -->\n\t\t\t{{ web_block(\n\t\t\t\t\"Section with Tabs\",\n\t\t\t\tvalues=tabs,\n\t\t\t\tadd_container=0,\n\t\t\t\tadd_top_padding=0,\n\t\t\t\tadd_bottom_padding=0\n\t\t\t) }}\n\t\t{% endif %}\n\t</div>\n</div>\n{% endblock %}"
  },
  {
    "path": "webshop/www/shop-by-category/index.js",
    "content": "$(() => {\n\t$('.category-card').on('click', (e) => {\n\t\tlet category_type = e.currentTarget.dataset.type;\n\t\tlet category_name = e.currentTarget.dataset.name;\n\n\t\tif (category_type != \"item_group\") {\n\t\t\tlet filters = {};\n\t\t\tfilters[category_type] =  [category_name];\n\t\t\twindow.location.href = \"/all-products?field_filters=\" + JSON.stringify(filters);\n\t\t}\n\t});\n});"
  },
  {
    "path": "webshop/www/shop-by-category/index.py",
    "content": "import frappe\nfrom frappe import _\n\nsitemap = 1\n\n\ndef get_context(context):\n\tcontext.body_class = \"product-page\"\n\n\tsettings = frappe.get_cached_doc(\"Webshop Settings\")\n\tcontext.categories_enabled = settings.enable_field_filters\n\n\tif context.categories_enabled:\n\t\tcategories = [row.fieldname for row in settings.filter_fields]\n\t\tcontext.tabs = get_tabs(categories)\n\n\tif settings.slideshow:\n\t\tcontext.slideshow = get_slideshow(settings.slideshow)\n\n\tcontext.no_cache = 1\n\n\ndef get_slideshow(slideshow):\n\tvalues = {\"show_indicators\": 1, \"show_controls\": 1, \"rounded\": 1, \"slider_name\": \"Categories\"}\n\tslideshow = frappe.get_cached_doc(\"Website Slideshow\", slideshow)\n\tslides = slideshow.get({\"doctype\": \"Website Slideshow Item\"})\n\tfor index, slide in enumerate(slides, start=1):\n\t\tvalues[f\"slide_{index}_image\"] = slide.image\n\t\tvalues[f\"slide_{index}_title\"] = slide.heading\n\t\tvalues[f\"slide_{index}_subtitle\"] = slide.description\n\t\tvalues[f\"slide_{index}_theme\"] = slide.get(\"theme\") or \"Light\"\n\t\tvalues[f\"slide_{index}_content_align\"] = slide.get(\"content_align\") or \"Centre\"\n\t\tvalues[f\"slide_{index}_primary_action\"] = slide.url\n\n\treturn values\n\n\ndef get_tabs(categories):\n\ttab_values = {\n\t\t\"title\": _(\"Shop by Category\"),\n\t}\n\n\tcategorical_data = get_category_records(categories)\n\tfor index, tab in enumerate(categorical_data, start=1):\n\t\ttab_values[f\"tab_{index + 1}_title\"] = frappe.unscrub(tab)\n\t\t# pre-render cards for each tab\n\t\ttab_values[f\"tab_{index + 1}_content\"] = frappe.render_template(\n\t\t\t\"webshop/www/shop-by-category/category_card_section.html\",\n\t\t\t{\"data\": categorical_data[tab], \"type\": tab},\n\t\t)\n\treturn tab_values\n\n\ndef get_category_records(categories):\n\tcategorical_data = {}\n\twebsite_item_meta = frappe.get_meta(\"Website Item\", cached=True)\n\n\tfor category in categories:\n\t\tif category == \"item_group\":\n\t\t\tcategorical_data[\"item_group\"] = frappe.db.get_all(\n\t\t\t\t\"Item Group\",\n\t\t\t\tfilters={\"show_in_website\": 1},\n\t\t\t\tfields=[\"name\", \"parent_item_group\", \"is_group\", \"image\", \"route\"],\n\t\t\t)\n\t\telse:\n\t\t\tfield_type = website_item_meta.get_field(category).fieldtype\n\n\t\t\tif field_type == \"Table MultiSelect\":\n\t\t\t\tchild_doc = website_item_meta.get_field(category).options\n\t\t\t\tfor field in frappe.get_meta(child_doc, cached=True).fields:\n\t\t\t\t\tif field.fieldtype == \"Link\" and field.reqd:\n\t\t\t\t\t\tdoctype = field.options\n\t\t\telse:\n\t\t\t\tdoctype = website_item_meta.get_field(category).options\n\n\t\t\tfields = [\"name\"]\n\n\t\t\tmeta = frappe.get_meta(doctype, cached=True)\n\t\t\tif meta.get_field(\"image\"):\n\t\t\t\tfields += [\"image\"]\n\n\t\t\tfilters = {}\n\t\t\tif meta.get_field(\"show_in_website\"):\n\t\t\t\tfilters = {\"show_in_website\": 1}\n\n\t\t\telif meta.get_field(\"custom_show_in_website\"):\n\t\t\t\tfilters = {\"custom_show_in_website\": 1}\n\n\t\t\ttry:\n\t\t\t\tif filters:\n\t\t\t\t\tcategorical_data[category] = frappe.db.get_all(doctype, fields=fields, filters=filters)\n\t\t\t\telse:\n\t\t\t\t\tcategorical_data[category] = frappe.db.get_all(doctype, fields=fields)\n\n\t\t\texcept BaseException:\n\t\t\t\tfrappe.throw(_(\"DocType {} not found\").format(doctype))\n\n\treturn categorical_data\n"
  }
]