diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2663579df..cfad5944f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -563,3 +563,10 @@ test_tender_cfaselectionua_2020_04_19: extends: - .test_before_2020_04_19 - .test_tender_cfaselectionua + +test_tender_pricequotation: + extends: .test + variables: + TESTS_PATH: src/openprocurement/tender/pricequotation/tests + COV_PATH: src/openprocurement/tender/pricequotation + COV_FILE: .coveragerc diff --git a/docker-compose.yml b/docker-compose.yml index 035616adc6..cae796e02e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,8 +25,8 @@ services: - "./docs:/app/docs:delegated" couchdb: image: "couchdb:1.6" + ports: + - "5984:5984" environment: COUCHDB_USER: op COUCHDB_PASSWORD: op - ports: - - "5984:5984" \ No newline at end of file diff --git a/docs/source/locale/uk/LC_MESSAGES/standard/qualificationmilestone.po b/docs/source/locale/uk/LC_MESSAGES/standard/qualificationmilestone.po index 70c233b7f2..ffb3663ee4 100644 --- a/docs/source/locale/uk/LC_MESSAGES/standard/qualificationmilestone.po +++ b/docs/source/locale/uk/LC_MESSAGES/standard/qualificationmilestone.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: openprocurement.api 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-10 17:53+0300\n" -"PO-Revision-Date: 2020-05-10 18:04+0300\n" +"POT-Creation-Date: 2020-06-01 12:43+0300\n" +"PO-Revision-Date: 2020-04-23 11:11+0300\n" "Last-Translator: \n" "Language: uk_UA\n" "Language-Team: \n" @@ -63,3 +63,4 @@ msgstr "рядок, :ref:`date`, генерується автоматично" msgid "date" msgstr "" + diff --git a/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/index.po b/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/index.po new file mode 100644 index 0000000000..6f633813a9 --- /dev/null +++ b/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/index.po @@ -0,0 +1,28 @@ +# Copyright (C) +# This file is distributed under the same license as the openprocurement.api package. +# +# FIRST AUTHOR , 2020. +# Oleh Helesh , 2020. +msgid "" +msgstr "" +"Project-Id-Version: openprocurement.api 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-01 12:43+0300\n" +"PO-Revision-Date: 2020-06-01 12:47+0200\n" +"Last-Translator: Oleh Helesh \n" +"Language-Team: English \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" +"Language: en_US\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 2.0\n" + +msgid "Price Quotation procedure" +msgstr "Процедура Запиту Цінових Пропозицій (`Price Quotation`)" + +msgid "Contents:" +msgstr "Зміст:" + + diff --git a/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/overview.po b/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/overview.po new file mode 100644 index 0000000000..6efc7e0b4f --- /dev/null +++ b/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/overview.po @@ -0,0 +1,131 @@ +# Copyright (C) +# This file is distributed under the same license as the openprocurement.api package. +# +# FIRST AUTHOR , 2020. +# Oleh Helesh , 2020. +msgid "" +msgstr "" +"Project-Id-Version: openprocurement.api 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-01 12:43+0300\n" +"PO-Revision-Date: 2020-06-11 17:26+0200\n" +"Last-Translator: Oleh Helesh \n" +"Language-Team: Ukrainian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" +"Language: uk\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<" +"=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Lokalize 2.0\n" + +msgid "Overview" +msgstr "Огляд" + +msgid "" +"The Open Procurement `Price Quotation` procedure is plugin to `Open" +" Procurement API` software." +msgstr "" +"The Open Procurement `Price Quotation` процедура - це плагін до `Open" +" Procurement API`." + +msgid "" +"REST-ful interface to plugin is in line with core software design principles." +msgstr "" +"REST-ful інтерфейс для плагіна відповідає основним принципам дизайну" +" програмного забезпечення." + +msgid "Main responsibilities" +msgstr "Основні задачі" + +msgid "" +"Price Quotation procedure is dedicated to Open Tender procedure for Ukrainian" +" below threshold procurements. The code for that type of procedure is" +" `priceQuotation`." +msgstr "" +"Процедура Запиту цінових пропозицій що описана в цій документації, стосується" +" допорогових закупівель від 50 тис. грн. до 200 тис. грн. Код процедури" +" такого типу: ``priceQuotation``" + +msgid "Business logic" +msgstr "Бізнес логіка" + +msgid "Publication of the Price Quotation" +msgstr "Публікація Запиту цінових пропозицій" + +msgid "" +"Business process begins when the Procuring Entity creates a Price Quotation" +" procedure using parameters from the e-Catalogues Profile database." +msgstr "" +"Бізнес процес починається при створенні процедури Запиту цінових пропозицій" +" Замовником, використовуючи параметри з бази даних електронних каталогів." + +msgid "" +"After Procuring Entity supplements the procedure with quantity of items and" +" delivery details and publishes the tender by sending a request for Price" +" Quotation to ProZorro Business Process Engine the process starts." +msgstr "" +"Після цього Замовник доповнює процедуру кількістю товарів та реквізитами" +" доставки та опобліковує процедуру закупівлю, шляхом надсилання запиту до" +" Business Process Engine Прозорро." + +msgid "" +"At this moment Business Process Engine receives and validates the Price" +" Quotation request. Given the validation is passed the system automatically" +" informs shortlisted (qualified to specific eCatalogue Profile) suppliers" +" about the request." +msgstr "" +"В цей момент Business Process Engine отримує та проводить перевірку Запиту" +" цінових пропозицій. Після успішного проходження перевірки система" +" автоматично повідомляє усіх shortlisted `(кваліфікованих до конкретного" +" профілю у каталогах)` постачальників про запит." + +msgid "Tendering" +msgstr "Тендерний процес" + +msgid "" +"Receiving a Price Quotation request, supplier decides if they are able to" +" offer the requested product. In case of rejection supplier declines" +" participation in procedure. Until the end of tender period (minimal two" +" working days) suppliers would be able to submit a bid, while BPE will" +" collect and register quotations." +msgstr "" +"Отримуючи Запит цінової пропозиції, постачальник приймає рішення про" +" готовність запропонувати запитуваний товар. У випадку відмови від запиту" +" постачальник відмовляється приймати участь у закупівлі. " +"До кінця тендерного періоду `(мінімально два робочих дні)` постачальник має" +" надати свою пропозиції, які буде приймати та реєструвати Business Process" +" Engine Прозорро." + +msgid "Awarding, Qualification" +msgstr "Процес визначення переможця" + +msgid "" +"After the deadline system will publish received bids, awarding suppleir with" +" most economically advantageous bid allowing to confirm award within two" +" business days. In case if award was not confirmed system will automatically" +" award next supplier providing same confirmation period. In case of no" +" suppliers left system will transfer procedure to status `unsuccessful`." +msgstr "" +"Після закінчення тендерного періоду система опоблікує отримані пропозиції," +" нагороджуючи постачальника з найбільш економічно вигідною пропозицією," +" надаючи два робочих дні на підтвердження нагороди. У випадку якщо нагорода" +" не була підтверджена, система автоматично нагороджує наступного" +" постачальника, надаючи рівноцінний час на підтвердження. У випадку" +" відсутності наступних пропозицій постачальників система переведе процедуру у" +" стан `unsuccesful`." + +msgid "Contracting" +msgstr "Укладання контракту" + +msgid "" +"Selecting a winner will lead both Procuring Entity and supplier to the" +" contracting process, where the contract is signed, published and taken to" +" execution." +msgstr "" +"Підтвердження нагороди приведе Замовника і Постачальника до процесу укладення" +" контракту, в якому контракт підписується, опубліковується та береться у" +" виконання." + + diff --git a/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/tutorial.po b/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/tutorial.po new file mode 100644 index 0000000000..6c5d3edb9e --- /dev/null +++ b/docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/tutorial.po @@ -0,0 +1,415 @@ +# Copyright (C) +# This file is distributed under the same license as the openprocurement.api package. +# +# FIRST AUTHOR , 2020. +# Oleh Helesh , 2020. +msgid "" +msgstr "" +"Project-Id-Version: openprocurement.api 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-01 14:44+0300\n" +"PO-Revision-Date: 2020-06-03 12:39+0200\n" +"Last-Translator: Oleh Helesh \n" +"Language: en_US\n" +"Language-Team: English \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" +"X-Generator: Lokalize 2.0\n" + +msgid "Tutorial" +msgstr "Туторіал" + +msgid "Exploring basic rules" +msgstr "Базові правила" + +msgid "Let's try exploring the `/tenders` endpoint:" +msgstr "Подивимось як працює точка входу `/tenders`:" + +msgid "Just invoking it reveals empty set." +msgstr "При виклику видає пустий набір." + +msgid "Now let's attempt creating some tender:" +msgstr "Спробуймо створити нову закупівлю:" + +msgid "Error states that the only accepted Content-Type is `application/json`." +msgstr "Помилка вказує, що єдиний прийнятний тип вмісту це `application/json`." + +msgid "Let's satisfy the Content-type requirement:" +msgstr "Задовільнимо вимогу типу вмісту:" + +msgid "Error states that no `data` has been found in JSON body." +msgstr "Помилка вказує, що `data` не знайдено у тілі JSON." + +msgid "Creating tender" +msgstr "Створення закупівлі" + +msgid "Let's provide the data attribute in the submitted body :" +msgstr "Введемо data атрибут у поданому тілі:" + +msgid "" +"Success! Now we can see that new object was created. Response code is `201`" +" and `Location` response header reports the location of the created object. " +" The body of response reveals the information about the created tender: its" +" internal `id` (that matches the `Location` segment), its official `tenderID`" +" and `dateModified` datestamp stating the moment in time when tender was last" +" modified. Note that tender is created with `draft` status." +msgstr "" +"Успіх! Тепер ми бачимо, що новий об’єкт було створено. Код відповіді `201` та" +" заголовок відповіді `Location` вказує місцерозташування створеного об’єкта." +" Тіло відповіді показує інформацію про створену закупівлю, її внутрішнє `id`" +" (яке співпадає з сегментом `Location`), її офіційне `tenderID` та" +" `dateModified` дату, що показує час, коли закупівля востаннє модифікувалась." +" Зверніть увагу, що закупівля створюється зі статусом `draft`." + +msgid "" +"**! Note:** User receives `access`: `token`:" +" ``\"151a30932ee245e989771be867bc8235\"`` with which operations as a" +" `Procuring Entity` role are accessible." +msgstr "" +"**! Примітка:** Користувач отримує `access`: `token`:" +" ``\"151a30932ee245e989771be867bc8235\"`` з яким доступні операції ролі" +" Замовника." + +msgid "" +"Price Quotation procedure has ``procurementMethodType``: ``priceQuotation``" +" and ``procurementMethod``: ``selective``." +msgstr "" +"Процедура Запиту цінових пропозицій має ``procurementMethodType``:" +" ``priceQuotation`` та ``procurementMethod``: ``selective``." + +msgid "" +"Let's access the URL of the created object (the `Location` header of the" +" response):" +msgstr "Використаємо URL створеного об’єкта (заголовок відповіді `Location`):" + +msgid "We can see the same response we got after creating tender." +msgstr "Ми бачимо ту ж відповідь, що і після створення закупівлі." + +msgid "Modifying tender" +msgstr "Модифікація закупівлі" + +msgid "" +"Procuring Entity can modify tender before publishing. Let's update tender by" +" supplementing it with all other essential properties:" +msgstr "" +"Замовник може відредагувати закупівлю перед публікацією. Давайте оновимо" +" закупівлю, доповнюючи її усіма іншими необхідними властивостями:" + +msgid "" +"We see the added properties have merged with existing tender data." +" Additionally, the `dateModified` property was updated to reflect the last" +" modification datestamp." +msgstr "" +"Ми бачимо, що додаткові властивості об’єднані з існуючими даними закупівлі." +" Додатково оновлена властивість `dateModified`, щоб відображати останню дату" +" модифікації." + +msgid "Checking the listing again reflects the new modification date:" +msgstr "Ще одна перевірка списку відображає нову дату модифікації:" + +msgid "Procuring entity can set bid guarantee:" +msgstr "Замовник може встановити забезпечення тендерної пропозиції:" + +msgid "Publishing tender" +msgstr "Публікація закупівлі" + +msgid "" +"After creation Procuring Entity publishes procedure by changing status to" +" `draft.publishing` where **priceQuotationBot** robot runs validation of the" +" procedure and supplement procedure with additional data taken from ProZorro" +" e-Catalogues database including `shortListedFirms`." +msgstr "" +"Після створення Замовник публікує процедуру, змінивши статус на" +" `draft.publishing`, де робот **priceQuotationBot** запускає перевірку" +" процедури та доповнює процедуру додатковими даними, отриманими з бази даних" +" електронних каталогів ProZorro, включаючи `shortListedFirms`." + +msgid "" +"After successful validation priceQuotationBot transmit procedure to status:" +" `active.tendering`" +msgstr "" +"Після успішної валідації priceQuotationBot переводить процедуру в статус:" +" `active.tendering`" + +#, fuzzy +msgid "" +"In case if procedure do not pass validation due to invalid options, it will" +" be switched to status: `draft.unsuccessful` by the **priceQuotationBot**." +msgstr "" +"У разі, якщо процедура не пройде перевірку через недійсні параметри, вона" +" буде переведена в статус: `draft.unsuccessful` за допомогою" +" priceQuotationBot." + +msgid "Bid submission" +msgstr "Подача пропозицій" + +msgid "Registering bid" +msgstr "Реєстрація пропозиції" + +msgid "Tender status ``active.tendering`` allows registration of bids." +msgstr "Статус закупівлі ``active.tendering`` дозволяє подання пропозицій." + +msgid "Bidder can register a bid with ``draft`` status:" +msgstr "" +"Учасник може зареєструвати пропозицію зі статусом ``draft`` (чернетка):" + +msgid "" +"**! Note:** User receives `access`: `token`:" +" ``\"00e173e5f31f4decbb811cc01e10c1bf\"`` with which operations as a" +" `Supplier` role are accessible." +msgstr "" +"**! Примітка:** Користувач отримує `access`: `token`:" +" ``\"151a30932ee245e989771be867bc8235\"`` з яким доступні операції ролі" +" Постачальника." + +msgid "And activate a bid:" +msgstr "Та активувати пропозицію:" + +msgid "Modifying bid" +msgstr "Модифікація пропозиції" + +msgid "Bid can be updated until the end of tender period." +msgstr "Пропозиція може бути оновленою до закінчення тендерного періоду." + +msgid "Proposal Uploading" +msgstr "Завантаження пропозиції" + +msgid "Then bidder should upload proposal document(s):" +msgstr "Потім учасник повинен завантажити документ(и) пропозиції:" + +msgid "It is possible to check the uploaded documents:" +msgstr "Можна перевірити завантажені документи:" + +msgid "Awarding process" +msgstr "Процес визначення переможця" + +msgid "" +"After the tender period end date, system automatically creates `award` in" +" `pending` status for the bid with the most economically advantageous price." +msgstr "" +"Після закінчення тендерного періоду, система автоматично створює ``award`` у" +" статусі ``pending`` для пропозиції з найбільш економічно вигідною ціною." + +msgid "" +"The Supplier-winner can accept `award` by transferring it to status:" +" `active`. The system is waiting for acceptance from the supplier-winner" +" within `two working days`." +msgstr "" +"Постачальник-переможець може підтвердити `award` змінивши його статус на" +" `active`. Система очікуватиме підтвердження від постачальника-переможця в" +" межах `двох робочих днів`." + +msgid "" +"Procuring Entity can cancel `award` after acceptance by changing `award`" +" status to `cancelled` in case if supplier-winner declines to sign contract." +msgstr "" +"Замовник може відмінити `award` після підтвердження змінивши його статус на" +" `cancelled` у випадку якщо постачальник-переможець відмовляється підписувати" +" контракт." + +msgid "" +"After canceling `award` system creates `second` `award` for the same bid in" +" status: `pending` with access for Procuring Entity only. By the decision of" +" Procuring Entity `second` `award` can be either changed for `active` or to" +" `unsuccessful` with ability to upload supplementary documents." +msgstr "" +"Після відміни `award`, система створює `другий` `award` для цієї пропозиції у" +" статусі `pending` з доступом лише Замовника. За рішенням Замовника `другий`" +" `award` може бути змінений на `active` або `unsuccessful` з можливістю" +" завантажити супровідну документацію." + +msgid "" +"The Supplier-winner can decline `award` by transferring it to status:" +" `unsuccessful`." +msgstr "" +"Постачальник-переможець може відмовитись від `award` змінивши його статус на " +" `unsuccessful`." + +msgid "" +"`Award` will be granted to the next bid with most economically advantageous" +" price, for the following cases:" +msgstr "" +"`Award` буде наданий до наступної пропозиції з найбільш економічно вигідною" +" ціною, у наступних випадках:" + +msgid "Supplier-winner didn't accept `award` within two working days." +msgstr "" +"Постачальник-переможець не підтвердив `award` в межах двох робочих днів. " + +msgid "Supplier-winner declined `award`." +msgstr "Постачальник-переможець відмовився від `award`." + +msgid "" +"Supplier-winner refused to sign contract and `award` was canceled by" +" Procuring Entity." +msgstr "" +"Постачальник-переможець відмовився підписувати контракт і `award` був" +" скасований Замовником." + +msgid "" +"**Note !** In the case of `award` being transferred to `unsuccessful` status" +" for the last bid, procedure will inherit termination status:" +" **`unsuccessful`**." +msgstr "" +"**! Примітка:** У випадку переходу `award` останньої пропозиції у статус" +" `unsuccessful` процедура набуде кінцевий статус: **`unsuccessful`**." + +msgid "Setting contract" +msgstr "Налаштування угоди" + +msgid "Setting contract value" +msgstr "Встановлення вартості угоди" + +msgid "" +"By default contract value is set based on the award, but there is a" +" possibility to set custom contract value." +msgstr "" +"За замовчуванням вартість угоди встановлюється на основі рішення про" +" визначення переможця, але є можливість змінити це значення. " + +msgid "" +"If you want to **lower contract value**, you can insert new one into the" +" `amount` field." +msgstr "" +"Якщо ви хочете **знизити вартість угоди**, ви можете встановити нове значення" +" для поля `amount`." + +msgid "`200 OK` response was returned. The value was modified successfully." +msgstr "Було повернуто код відповіді `200 OK`. Значення змінено успішно." + +msgid "Setting contract signature date" +msgstr "Встановлення дати підписання угоди" + +msgid "" +"There is a possibility to set custom contract signature date. You can insert" +" appropriate date into the `dateSigned` field." +msgstr "" +"Є можливість встановити дату підписання угоди. Для цього вставте відповідну" +" дату в поле `dateSigned`." + +msgid "" +"If this date is not set, it will be auto-generated on the date of contract" +" registration." +msgstr "" +"Якщо ви не встановите дату підписання, то вона буде згенерована автоматично" +" під час реєстрації угоди." + +msgid "Setting contract validity period" +msgstr "Встановлення терміну дії угоди" + +msgid "" +"Setting contract validity period is optional, but if it is needed, you can" +" set appropriate `startDate` and `endDate`." +msgstr "" +"Встановлення терміну дії угоди необов’язкове, але, якщо є необхідність, ви" +" можете встановити відповідну дату початку `startDate` та кінця `endDate`" +" терміну дії." + +msgid "Uploading contract documentation" +msgstr "Завантаження документації по угоді" + +msgid "You can upload contract documents for the Price Quotation procedure." +msgstr "" +"Ви можете завантажити документи угоди для процедури Запиту цінових пропозицій." + +msgid "Let's upload contract document:" +msgstr "Завантажимо документ угоди:" + +msgid "" +"`201 Created` response code and `Location` header confirm that this document" +" was added." +msgstr "" +"`201` Використаємо URL створеного об’єкта (заголовок відповіді `Location`)." + +msgid "Let's view the uploaded contract document:" +msgstr "Подивимось на список документів пов’язаних з угодою:" + +msgid "Cancelling tender" +msgstr "Відміна закупівлі" + +msgid "" +"Tender creator can cancel tender anytime (except when tender in terminal" +" status e.g. `draft.unsuccessful`, `unsuccessful`, `cancelled`, `complete`)." +msgstr "" +"Замовник може скасувати закупівлю у будь-який момент (крім закупівель у" +" кінцевому стані, наприклад, `unsuccessful`, `cancelled`, `complete`)." + +msgid "The following steps should be applied:" +msgstr "Для цього потрібно виконати наступні кроки:" + +msgid "Prepare cancellation request." +msgstr "Приготуйте запит на скасування." + +msgid "Fill it with the protocol describing the cancellation reasons." +msgstr "Наповніть його протоколом про причини скасування." + +msgid "Cancel the tender with the prepared reasons." +msgstr "Скасуйте закупівлю через подані причини." + +msgid "" +"Only the request that has been activated (3rd step above) has power to cancel" +" tender. I.e. you have to not only prepare cancellation request but to" +" activate it as well." +msgstr "" +"Запит на скасування, який не пройшов активації (3-й крок), не матиме сили," +" тобто, для скасування закупівлі буде обов’язковим не тільки створити заявку," +" але і активувати її." + +msgid "" +"For cancelled cancellation you need to update cancellation status to" +" `unsuccessful` from `draft` or `pending`." +msgstr "" +"Для відміни скасування закупівлі, вам потрібно оновити статус скасування до" +" `unsuccessful` з `draft` чи `pending`" + +msgid "See :ref:`cancellation` data structure for details." +msgstr "" +"Дивіться структуру запиту :ref:`cancellation` для більш детальної інформації." + +msgid "Preparing the cancellation request" +msgstr "Формування запиту на скасування" + +msgid "" +"You should pass `reason` and `reasonType`, `status` defaults to `draft`." +msgstr "" +"Ви повинні передати змінні `reason` та `reasonType`, `status` у стані `draft`." + +msgid "" +"There are four possible types of cancellation reason - tender was `noDemand`," +" `unFixable`, `forceMajeure` and `expensesCut`." +msgstr "" +"При скасуванні, замовник має визначити один з чотирьох типів reasonType:" +" `noDemand`, `unFixable`, `forceMajeure` aбо `expensesCut`." + +msgid "`id` is autogenerated and passed in the `Location` header of response." +msgstr "" +"`id` генерується автоматично і повертається у додатковому заголовку відповіді" +" `Location`:" + +msgid "You can change ``reasonType`` value to any of the above." +msgstr "Ви можете виправити тип на будь-який що вказаний вище." + +msgid "Filling cancellation with protocol and supplementary documentation" +msgstr "Наповнення протоколом та іншою супровідною документацією" + +msgid "" +"This step is required. Without documents you can't update tender status." +msgstr "" +"Цей крок обов'язковий. Без документів ви не можете оновити статус закупівлі." + +msgid "Upload the file contents" +msgstr "Завантажити вміст файлу" + +msgid "Change the document description and other properties" +msgstr "Зміна опису документа та інших властивостей" + +msgid "Upload new version of the document" +msgstr "Завантажити нову версію документа" + +msgid "Activating the request and cancelling tender" +msgstr "Активація запиту на відміну закупівлі" + + diff --git a/docs/source/tendering/index.rst b/docs/source/tendering/index.rst index c153f9f2fe..aca1f07c9d 100644 --- a/docs/source/tendering/index.rst +++ b/docs/source/tendering/index.rst @@ -18,3 +18,4 @@ Contents: defense/index cfaua/index cfaselectionua/index + pricequotation/index diff --git a/docs/source/tendering/pricequotation/http/activate-bidder.http b/docs/source/tendering/pricequotation/http/activate-bidder.http new file mode 100644 index 0000000000..3b10c32517 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/activate-bidder.http @@ -0,0 +1,114 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272?acc_token=7ee1f0a7d9ca4a6d95fe2bf79100a9e1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 30 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "active" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "active", + "value": { + "currency": "UAH", + "amount": 459.0, + "valueAddedTaxIncluded": true + }, + "requirementResponses": [ + { + "requirement": { + "id": "655360-0001-001-01" + }, + "id": "04afe4402ce94a529c34e9ad56e44459", + "value": "23.8" + }, + { + "requirement": { + "id": "655360-0002-001-01" + }, + "id": "3d7a8e3017134a8a99b92810201c874d", + "value": "1920x1080" + }, + { + "requirement": { + "id": "655360-0003-001-01" + }, + "id": "2de49e0ef8464280a14caeae06165265", + "value": "16:9" + }, + { + "requirement": { + "id": "655360-0004-001-01" + }, + "id": "a05088c5214046ddb11e9f8fd4bce0ab", + "value": "250" + }, + { + "requirement": { + "id": "655360-0005-001-01" + }, + "id": "a7e6704a13ff4cd9b69fac3a55018f03", + "value": "1000:1" + }, + { + "requirement": { + "id": "655360-0006-001-01" + }, + "id": "4bcc030a105b4975bd2a757b5a494108", + "value": "1" + }, + { + "requirement": { + "id": "655360-0007-001-01" + }, + "id": "c20f0d05817647579c2f257ac8a7ac0a", + "value": "1" + }, + { + "requirement": { + "id": "655360-0008-001-01" + }, + "id": "70558fb7e25b4264a53b566aa8696c54", + "value": "HDMI" + }, + { + "requirement": { + "id": "655360-0009-001-01" + }, + "id": "e2a926693411449497151e6ac7b1f997", + "value": "36" + } + ], + "tenderers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "date": "2020-05-15T01:00:01+03:00", + "id": "e46b68ebf7724fcfa182eeade05ec272" + } +} + diff --git a/docs/source/tendering/pricequotation/http/active-cancellation.http b/docs/source/tendering/pricequotation/http/active-cancellation.http new file mode 100644 index 0000000000..e6076d0e85 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/active-cancellation.http @@ -0,0 +1,49 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 30 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "active" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "active", + "documents": [ + { + "hash": "md5:00000000000000000000000000000000", + "description": "Changed description", + "title": "Notice.pdf", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/6c09ba3998ad4cb8a1232d732d0b418a?KeyID=a8968c46&Signature=1kVtsM7NZS1ALmYHAnefygkvEQVOcy%2FvAWxtCk5HXRMuOKyfXo23XH8yrDAbKuFWAAJWkm1HYYgW7kPVMTSrDg%253D%253D", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "8eab55698e214da9a8596071cc90ed8c", + "dateModified": "2020-05-15T01:00:03+03:00" + }, + { + "hash": "md5:00000000000000000000000000000000", + "description": "Changed description", + "title": "Notice-2.pdf", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/4160874d71844a7c987fc08322d8b775?KeyID=a8968c46&Signature=d3ac3FsDW4CfeyBeKPkEuzY0tNdHnRpx%252BW%2FdGnY961d%252BwKR2ctPXMdR%252BYgWVBFcX5TcVrvIIpXTorapeIFdjCA%253D%253D", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "8eab55698e214da9a8596071cc90ed8c", + "dateModified": "2020-05-15T01:00:03+03:00" + } + ], + "reason": "cancellation reason", + "reasonType": "expensesCut", + "date": "2020-05-15T01:00:03+03:00", + "cancellationOf": "tender", + "id": "4b9cfb25db254d3ab12593f969cfc64d" + } +} + diff --git a/docs/source/tendering/pricequotation/http/award-active.http b/docs/source/tendering/pricequotation/http/award-active.http new file mode 100644 index 0000000000..1ea13d0f72 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/award-active.http @@ -0,0 +1,77 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/awards/5f64866dfe6941f2a9d069642cef4aaa?acc_token=adf39ec64c5247239b58aa72f5b14aa7 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 30 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "active" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "active", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "4a907324bf9b40f79a057716377707f7", + "value": { + "currency": "UAH", + "amount": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "5f64866dfe6941f2a9d069642cef4aaa" + } +} + diff --git a/docs/source/tendering/pricequotation/http/award-cancelled.http b/docs/source/tendering/pricequotation/http/award-cancelled.http new file mode 100644 index 0000000000..dbd8566f3d --- /dev/null +++ b/docs/source/tendering/pricequotation/http/award-cancelled.http @@ -0,0 +1,78 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/awards/5f64866dfe6941f2a9d069642cef4aaa?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 33 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "cancelled" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/awards/c58f6124caae4fa8aee6e33233bb4ba3 +{ + "data": { + "status": "cancelled", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "4a907324bf9b40f79a057716377707f7", + "value": { + "currency": "UAH", + "amount": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "5f64866dfe6941f2a9d069642cef4aaa" + } +} + diff --git a/docs/source/tendering/pricequotation/http/award-unsuccesful.http b/docs/source/tendering/pricequotation/http/award-unsuccesful.http new file mode 100644 index 0000000000..b5f8658c4b --- /dev/null +++ b/docs/source/tendering/pricequotation/http/award-unsuccesful.http @@ -0,0 +1,77 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/awards/fa58e89a4f484cf3aed305b53c911112?acc_token=7ee1f0a7d9ca4a6d95fe2bf79100a9e1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 36 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "unsuccessful" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "unsuccessful", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "e46b68ebf7724fcfa182eeade05ec272", + "value": { + "currency": "UAH", + "amount": 459.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "fa58e89a4f484cf3aed305b53c911112" + } +} + diff --git a/docs/source/tendering/pricequotation/http/awards-listing-after-cancel.http b/docs/source/tendering/pricequotation/http/awards-listing-after-cancel.http new file mode 100644 index 0000000000..d55982e037 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/awards-listing-after-cancel.http @@ -0,0 +1,131 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/awards HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": [ + { + "status": "unsuccessful", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "e46b68ebf7724fcfa182eeade05ec272", + "value": { + "currency": "UAH", + "amount": 459.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "fa58e89a4f484cf3aed305b53c911112" + }, + { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "4a907324bf9b40f79a057716377707f7", + "value": { + "currency": "UAH", + "amount": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "5f64866dfe6941f2a9d069642cef4aaa" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/awards-listing.http b/docs/source/tendering/pricequotation/http/awards-listing.http new file mode 100644 index 0000000000..f3abd89e31 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/awards-listing.http @@ -0,0 +1,131 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/awards HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": [ + { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "e46b68ebf7724fcfa182eeade05ec272", + "value": { + "currency": "UAH", + "amount": 459.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "fa58e89a4f484cf3aed305b53c911112" + }, + { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "bid_id": "4a907324bf9b40f79a057716377707f7", + "value": { + "currency": "UAH", + "amount": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "id": "5f64866dfe6941f2a9d069642cef4aaa" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/bidder-documents.http b/docs/source/tendering/pricequotation/http/bidder-documents.http new file mode 100644 index 0000000000..6c6ac1feca --- /dev/null +++ b/docs/source/tendering/pricequotation/http/bidder-documents.http @@ -0,0 +1,21 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272/documents?acc_token=7ee1f0a7d9ca4a6d95fe2bf79100a9e1 HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": [ + { + "hash": "md5:00000000000000000000000000000000", + "title": "Proposal.pdf", + "url": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272/documents/6f3bb464ac7844b7ac3136550e8751e4?download=fb1bfc41e45e4a6c946adbd59bfdff16", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:01+03:00", + "id": "6f3bb464ac7844b7ac3136550e8751e4", + "dateModified": "2020-05-15T01:00:01+03:00" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/blank-tender-view.http b/docs/source/tendering/pricequotation/http/blank-tender-view.http new file mode 100644 index 0000000000..5b0a2b076f --- /dev/null +++ b/docs/source/tendering/pricequotation/http/blank-tender-view.http @@ -0,0 +1,71 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397 HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "draft", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "startDate": "2020-05-15T01:00:00+03:00", + "endDate": "2020-05-29T01:00:00+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "description": "Комп’ютерне обладнання", + "quantity": 1.0, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000.0, + "valueAddedTaxIncluded": true + }, + "submissionMethod": "electronicAuction", + "date": "2020-05-15T01:00:00+03:00", + "profile": "655360-30230000-889652-40000777", + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + }, + "awardCriteria": "lowestCost", + "owner": "broker", + "dateModified": "2020-05-15T01:00:00+03:00", + "id": "2f6b50d7e72d4fc184f561d945af0397", + "tenderID": "UA-2020-05-15-000001" + } +} + diff --git a/docs/source/tendering/pricequotation/http/contract-listing.http b/docs/source/tendering/pricequotation/http/contract-listing.http new file mode 100644 index 0000000000..3a586d92a5 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/contract-listing.http @@ -0,0 +1,135 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": [ + { + "status": "cancelled", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "value": { + "currency": "UAH", + "amount": 479.0, + "amountNet": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "awardID": "5f64866dfe6941f2a9d069642cef4aaa", + "id": "323ed4ff03fe4844b0aea6fe131cc717", + "contractID": "UA-2020-05-15-000001-1" + }, + { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "value": { + "currency": "UAH", + "amount": 479.0, + "amountNet": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "awardID": "c58f6124caae4fa8aee6e33233bb4ba3", + "id": "081c725a9a7f49c08e30ff870b3c2d56", + "contractID": "UA-2020-05-15-000001-2" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/initial-tender-listing.http b/docs/source/tendering/pricequotation/http/initial-tender-listing.http new file mode 100644 index 0000000000..a5d2567b5e --- /dev/null +++ b/docs/source/tendering/pricequotation/http/initial-tender-listing.http @@ -0,0 +1,15 @@ +GET /api/2.5/tenders HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "next_page": { + "path": "/api/2.5/tenders?offset=", + "uri": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders?offset=", + "offset": "" + }, + "data": [] +} + diff --git a/docs/source/tendering/pricequotation/http/patch-bidder.http b/docs/source/tendering/pricequotation/http/patch-bidder.http new file mode 100644 index 0000000000..51f5e452b1 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/patch-bidder.http @@ -0,0 +1,116 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272?acc_token=7ee1f0a7d9ca4a6d95fe2bf79100a9e1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 36 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "value": { + "amount": 459 + } + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "draft", + "value": { + "currency": "UAH", + "amount": 459.0, + "valueAddedTaxIncluded": true + }, + "requirementResponses": [ + { + "requirement": { + "id": "655360-0001-001-01" + }, + "id": "04afe4402ce94a529c34e9ad56e44459", + "value": "23.8" + }, + { + "requirement": { + "id": "655360-0002-001-01" + }, + "id": "3d7a8e3017134a8a99b92810201c874d", + "value": "1920x1080" + }, + { + "requirement": { + "id": "655360-0003-001-01" + }, + "id": "2de49e0ef8464280a14caeae06165265", + "value": "16:9" + }, + { + "requirement": { + "id": "655360-0004-001-01" + }, + "id": "a05088c5214046ddb11e9f8fd4bce0ab", + "value": "250" + }, + { + "requirement": { + "id": "655360-0005-001-01" + }, + "id": "a7e6704a13ff4cd9b69fac3a55018f03", + "value": "1000:1" + }, + { + "requirement": { + "id": "655360-0006-001-01" + }, + "id": "4bcc030a105b4975bd2a757b5a494108", + "value": "1" + }, + { + "requirement": { + "id": "655360-0007-001-01" + }, + "id": "c20f0d05817647579c2f257ac8a7ac0a", + "value": "1" + }, + { + "requirement": { + "id": "655360-0008-001-01" + }, + "id": "70558fb7e25b4264a53b566aa8696c54", + "value": "HDMI" + }, + { + "requirement": { + "id": "655360-0009-001-01" + }, + "id": "e2a926693411449497151e6ac7b1f997", + "value": "36" + } + ], + "tenderers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "date": "2020-05-15T01:00:01+03:00", + "id": "e46b68ebf7724fcfa182eeade05ec272" + } +} + diff --git a/docs/source/tendering/pricequotation/http/patch-cancellation.http b/docs/source/tendering/pricequotation/http/patch-cancellation.http new file mode 100644 index 0000000000..f9b19b1ba2 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/patch-cancellation.http @@ -0,0 +1,28 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d/documents/8eab55698e214da9a8596071cc90ed8c?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 48 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "description": "Changed description" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "hash": "md5:00000000000000000000000000000000", + "description": "Changed description", + "title": "Notice.pdf", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/6c09ba3998ad4cb8a1232d732d0b418a?KeyID=a8968c46&Signature=1kVtsM7NZS1ALmYHAnefygkvEQVOcy%2FvAWxtCk5HXRMuOKyfXo23XH8yrDAbKuFWAAJWkm1HYYgW7kPVMTSrDg%253D%253D", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "8eab55698e214da9a8596071cc90ed8c", + "dateModified": "2020-05-15T01:00:03+03:00" + } +} + diff --git a/docs/source/tendering/pricequotation/http/patch-tender-data.http b/docs/source/tendering/pricequotation/http/patch-tender-data.http new file mode 100644 index 0000000000..292f7957d2 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/patch-tender-data.http @@ -0,0 +1,81 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 68 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "tenderPeriod": { + "endDate": "2020-05-30T01:00:11+03:00" + } + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "draft", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "startDate": "2020-05-15T01:00:00+03:00", + "endDate": "2020-05-30T01:00:11+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "description": "Комп’ютерне обладнання", + "quantity": 1.0, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000.0, + "valueAddedTaxIncluded": true + }, + "submissionMethod": "electronicAuction", + "date": "2020-05-15T01:00:00+03:00", + "profile": "655360-30230000-889652-40000777", + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + }, + "awardCriteria": "lowestCost", + "owner": "broker", + "dateModified": "2020-05-15T01:00:01+03:00", + "id": "2f6b50d7e72d4fc184f561d945af0397", + "tenderID": "UA-2020-05-15-000001" + } +} + diff --git a/docs/source/tendering/pricequotation/http/prepare-cancellation.http b/docs/source/tendering/pricequotation/http/prepare-cancellation.http new file mode 100644 index 0000000000..6a90ef9ec6 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/prepare-cancellation.http @@ -0,0 +1,27 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 69 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "reason": "cancellation reason", + "reasonType": "noDemand" + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d +{ + "data": { + "status": "draft", + "reason": "cancellation reason", + "reasonType": "noDemand", + "date": "2020-05-15T01:00:03+03:00", + "cancellationOf": "tender", + "id": "4b9cfb25db254d3ab12593f969cfc64d" + } +} + diff --git a/docs/source/tendering/pricequotation/http/publish-tender.http b/docs/source/tendering/pricequotation/http/publish-tender.http new file mode 100644 index 0000000000..261749a2a2 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/publish-tender.http @@ -0,0 +1,80 @@ +PATCH /api/2.5/tenders/195eb794810a428e8ecfea8e721dc668?acc_token=fe46fdfa45b14ff3aac656bd1f686571 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 40 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "draft.publishing" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "draft.publishing", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "startDate": "2020-05-15T01:00:00+03:00", + "endDate": "2020-05-29T01:00:00+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "description": "Комп’ютерне обладнання", + "quantity": 1.0, + "id": "e2ead0c612654db3af732ffd1749ab0e", + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000.0, + "valueAddedTaxIncluded": true + }, + "submissionMethod": "electronicAuction", + "date": "2020-05-15T01:00:00+03:00", + "noticePublicationDate": "2020-05-15T01:00:00+03:00", + "profile": "655360-30230000-889652-40000777", + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + }, + "awardCriteria": "lowestCost", + "owner": "broker", + "dateModified": "2020-05-15T01:00:00+03:00", + "id": "195eb794810a428e8ecfea8e721dc668", + "tenderID": "UA-2020-05-15-000001" + } +} + diff --git a/docs/source/tendering/pricequotation/http/register-2nd-bidder.http b/docs/source/tendering/pricequotation/http/register-2nd-bidder.http new file mode 100644 index 0000000000..d73aff59a3 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/register-2nd-bidder.http @@ -0,0 +1,237 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids HTTP/1.0 +Authorization: Bearer broker +Content-Length: 2162 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "requirementResponses": [ + { + "requirement": { + "id": "655360-0001-001-01" + }, + "value": "23.8" + }, + { + "requirement": { + "id": "655360-0002-001-01" + }, + "value": "1920x1080" + }, + { + "requirement": { + "id": "655360-0003-001-01" + }, + "value": "16:9" + }, + { + "requirement": { + "id": "655360-0004-001-01" + }, + "value": 250 + }, + { + "requirement": { + "id": "655360-0005-001-01" + }, + "value": "1000:1" + }, + { + "requirement": { + "id": "655360-0006-001-01" + }, + "value": 1 + }, + { + "requirement": { + "id": "655360-0007-001-01" + }, + "value": 1 + }, + { + "requirement": { + "id": "655360-0008-001-01" + }, + "value": "HDMI" + }, + { + "requirement": { + "id": "655360-0009-001-01" + }, + "value": 36 + } + ], + "documents": [ + { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/03950b53fab04d3aa37fa2ae2a628566?KeyID=a8968c46&Signature=a4W3uzYak7KWMeye0tzhB%2Fyvn%2BclCijMddloGrpWPVgycUbe7j8I%2BCrXcDDaCZWZhEoynxj09C8J2eU69EExCw%3D%3D", + "title": "Proposal_part1.pdf", + "hash": "md5:00000000000000000000000000000000", + "format": "application/pdf" + }, + { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/e1978610003e4991ba4cf1e89b934836?KeyID=a8968c46&Signature=zaSQ3jzZOCg7ktRRI3hvsUkpg6k3DkaI1LuLKaYDq7oC5FUOoEVTXYkhpvHJT4KQhDS0kp2pS%2FauUF3vVsY8DA%3D%3D", + "title": "Proposal_part2.pdf", + "hash": "md5:00000000000000000000000000000000", + "format": "application/pdf" + } + ], + "value": { + "currency": "UAH", + "amount": 479, + "valueAddedTaxIncluded": true + }, + "tenderers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "countryName": "Україна", + "postalCode": "01220", + "region": "м. Київ", + "streetAddress": "вул. Банкова, 11, корпус 1", + "locality": "м. Київ" + } + } + ] + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/4a907324bf9b40f79a057716377707f7 +{ + "access": { + "transfer": "4756d5cbc5f14b62b2a12df7c66c4902", + "token": "adf39ec64c5247239b58aa72f5b14aa7" + }, + "data": { + "status": "active", + "documents": [ + { + "hash": "md5:00000000000000000000000000000000", + "title": "Proposal_part1.pdf", + "url": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/4a907324bf9b40f79a057716377707f7/documents/419ac6510c43416591fd31a6b49e7b71?download=03950b53fab04d3aa37fa2ae2a628566", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:01+03:00", + "id": "419ac6510c43416591fd31a6b49e7b71", + "dateModified": "2020-05-15T01:00:01+03:00" + }, + { + "hash": "md5:00000000000000000000000000000000", + "title": "Proposal_part2.pdf", + "url": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/4a907324bf9b40f79a057716377707f7/documents/9da2183f3e1e4dad9e251c61b04a90d3?download=e1978610003e4991ba4cf1e89b934836", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:01+03:00", + "id": "9da2183f3e1e4dad9e251c61b04a90d3", + "dateModified": "2020-05-15T01:00:01+03:00" + } + ], + "value": { + "currency": "UAH", + "amount": 479.0, + "valueAddedTaxIncluded": true + }, + "requirementResponses": [ + { + "requirement": { + "id": "655360-0001-001-01" + }, + "id": "2fce1dece7694a008082d287f774c2bd", + "value": "23.8" + }, + { + "requirement": { + "id": "655360-0002-001-01" + }, + "id": "423a1bdb4ce84c778cb8f1c13ee9fe39", + "value": "1920x1080" + }, + { + "requirement": { + "id": "655360-0003-001-01" + }, + "id": "ac0e69389ccf4d018a71e34fff125f8a", + "value": "16:9" + }, + { + "requirement": { + "id": "655360-0004-001-01" + }, + "id": "569c1cf4595f4cd79a01b7de3eb2f435", + "value": "250" + }, + { + "requirement": { + "id": "655360-0005-001-01" + }, + "id": "255e1494f39a4da1a5b320f4d5be4d83", + "value": "1000:1" + }, + { + "requirement": { + "id": "655360-0006-001-01" + }, + "id": "e22577c7342d4cf883cda69e1f429a13", + "value": "1" + }, + { + "requirement": { + "id": "655360-0007-001-01" + }, + "id": "6e258ce034be4c0bafc42a9ff7678ed4", + "value": "1" + }, + { + "requirement": { + "id": "655360-0008-001-01" + }, + "id": "14557535923d4d17a59e760226612729", + "value": "HDMI" + }, + { + "requirement": { + "id": "655360-0009-001-01" + }, + "id": "8c65f83da80646d5888e5c98887a8b42", + "value": "36" + } + ], + "tenderers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "date": "2020-05-15T01:00:01+03:00", + "id": "4a907324bf9b40f79a057716377707f7" + } +} + diff --git a/docs/source/tendering/pricequotation/http/register-bidder.http b/docs/source/tendering/pricequotation/http/register-bidder.http new file mode 100644 index 0000000000..3cf7562663 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/register-bidder.http @@ -0,0 +1,202 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids HTTP/1.0 +Authorization: Bearer broker +Content-Length: 1522 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "draft", + "requirementResponses": [ + { + "requirement": { + "id": "655360-0001-001-01" + }, + "value": "23.8" + }, + { + "requirement": { + "id": "655360-0002-001-01" + }, + "value": "1920x1080" + }, + { + "requirement": { + "id": "655360-0003-001-01" + }, + "value": "16:9" + }, + { + "requirement": { + "id": "655360-0004-001-01" + }, + "value": 250 + }, + { + "requirement": { + "id": "655360-0005-001-01" + }, + "value": "1000:1" + }, + { + "requirement": { + "id": "655360-0006-001-01" + }, + "value": 1 + }, + { + "requirement": { + "id": "655360-0007-001-01" + }, + "value": 1 + }, + { + "requirement": { + "id": "655360-0008-001-01" + }, + "value": "HDMI" + }, + { + "requirement": { + "id": "655360-0009-001-01" + }, + "value": 36 + } + ], + "value": { + "currency": "UAH", + "amount": 469, + "valueAddedTaxIncluded": true + }, + "tenderers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "countryName": "Україна", + "postalCode": "01220", + "region": "м. Київ", + "streetAddress": "вул. Банкова, 11, корпус 1", + "locality": "м. Київ" + } + } + ] + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272 +{ + "access": { + "transfer": "f53b0359c5fe462f8eee71b2de180b53", + "token": "7ee1f0a7d9ca4a6d95fe2bf79100a9e1" + }, + "data": { + "status": "draft", + "value": { + "currency": "UAH", + "amount": 469.0, + "valueAddedTaxIncluded": true + }, + "requirementResponses": [ + { + "requirement": { + "id": "655360-0001-001-01" + }, + "id": "04afe4402ce94a529c34e9ad56e44459", + "value": "23.8" + }, + { + "requirement": { + "id": "655360-0002-001-01" + }, + "id": "3d7a8e3017134a8a99b92810201c874d", + "value": "1920x1080" + }, + { + "requirement": { + "id": "655360-0003-001-01" + }, + "id": "2de49e0ef8464280a14caeae06165265", + "value": "16:9" + }, + { + "requirement": { + "id": "655360-0004-001-01" + }, + "id": "a05088c5214046ddb11e9f8fd4bce0ab", + "value": "250" + }, + { + "requirement": { + "id": "655360-0005-001-01" + }, + "id": "a7e6704a13ff4cd9b69fac3a55018f03", + "value": "1000:1" + }, + { + "requirement": { + "id": "655360-0006-001-01" + }, + "id": "4bcc030a105b4975bd2a757b5a494108", + "value": "1" + }, + { + "requirement": { + "id": "655360-0007-001-01" + }, + "id": "c20f0d05817647579c2f257ac8a7ac0a", + "value": "1" + }, + { + "requirement": { + "id": "655360-0008-001-01" + }, + "id": "70558fb7e25b4264a53b566aa8696c54", + "value": "HDMI" + }, + { + "requirement": { + "id": "655360-0009-001-01" + }, + "id": "e2a926693411449497151e6ac7b1f997", + "value": "36" + } + ], + "tenderers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "date": "2020-05-15T01:00:01+03:00", + "id": "e46b68ebf7724fcfa182eeade05ec272" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-after-bot-active.http b/docs/source/tendering/pricequotation/http/tender-after-bot-active.http new file mode 100644 index 0000000000..3b13c9ba12 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-after-bot-active.http @@ -0,0 +1,335 @@ +GET /api/2.5/tenders/195eb794810a428e8ecfea8e721dc668 HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "active.tendering", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "startDate": "2020-05-15T01:00:00+03:00", + "endDate": "2020-05-29T01:00:00+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "e2ead0c612654db3af732ffd1749ab0e", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000.0, + "valueAddedTaxIncluded": true + }, + "submissionMethod": "electronicAuction", + "shortlistedFirms": [ + { + "status": "active", + "scale": "large", + "name": "Товариство з обмеженою відповідальністю «Пікселі»", + "address": { + "postalCode": "01100", + "countryName": "Україна", + "streetAddress": "бул.Дружби Народів, 8", + "region": "Київська область", + "locality": "м.Київ" + }, + "contactPoint": { + "email": "contact@pixel.pix", + "telephone": "(067) 123-45-67", + "name": "Оксана Піксель" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "12345678", + "legalName": "Товариство з обмеженою відповідальністю «Пікселі»" + }, + "id": "UA-EDR-12345678" + }, + { + "status": "active", + "scale": "large", + "name": "Товариство з обмеженою відповідальністю «Штекер-Пекер»", + "address": { + "postalCode": "46000", + "countryName": "Україна", + "streetAddress": "вул. Кластерна, 777-К", + "region": "Тернопільська область", + "locality": "м.Тернопіль" + }, + "contactPoint": { + "email": "info@shteker.pek", + "telephone": "(095) 123-45-67", + "name": "Олег Штекер" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "87654321", + "legalName": "Товариство з обмеженою відповідальністю «Штекер-Пекер»" + }, + "id": "UA-EDR-87654321" + } + ], + "date": "2020-05-15T01:00:00+03:00", + "noticePublicationDate": "2020-05-15T01:00:00+03:00", + "profile": "655360-30230000-889652-40000777", + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + }, + "dateModified": "2020-05-15T01:00:00+03:00", + "criteria": [ + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "number", + "title": "Діагональ екрану", + "id": "655360-0001-001-01", + "unit": { + "code": "INH", + "name": "дюйм" + }, + "minValue": "23.8" + } + ], + "id": "655360-0001-001", + "description": "Діагональ екрану, не менше 23.8 дюймів" + } + ], + "title": "Діагональ екрану", + "description": "Діагональ екрану", + "id": "655360-0001" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "string", + "id": "655360-0002-001-01", + "expectedValue": "1920x1080", + "title": "Роздільна здатність" + } + ], + "id": "655360-0002-001", + "description": "Роздільна здатність - 1920x1080" + } + ], + "title": "Роздільна здатність", + "description": "Роздільна здатність", + "id": "655360-0002" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "string", + "id": "655360-0003-001-01", + "expectedValue": "16:9", + "title": "Співвідношення сторін" + } + ], + "id": "655360-0003-001", + "description": "Співвідношення сторін" + } + ], + "title": "Співвідношення сторін", + "description": "Співвідношення сторін", + "id": "655360-0003" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "integer", + "title": "Яскравість дисплея", + "id": "655360-0004-001-01", + "unit": { + "code": "A24", + "name": "кд/м²" + }, + "minValue": "250" + } + ], + "id": "655360-0004-001", + "description": "Яскравість дисплея, не менше 250 кд/м²" + } + ], + "title": "Яскравість дисплея", + "description": "Яскравість дисплея", + "id": "655360-0004" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "string", + "id": "655360-0005-001-01", + "expectedValue": "1000:1", + "title": "Контрастність (статична)" + } + ], + "id": "655360-0005-001", + "description": "Контрастність (статична) - 1000:1" + }, + { + "requirements": [ + { + "dataType": "string", + "id": "655360-0005-002-01", + "expectedValue": "3000:1", + "title": "Контрастність (статична)" + } + ], + "id": "655360-0005-002", + "description": "Контрастність (статична) - 3000:1" + } + ], + "title": "Контрастність (статична)", + "description": "Контрастність (статична)", + "id": "655360-0005" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "integer", + "title": "Кількість портів HDMI", + "id": "655360-0006-001-01", + "unit": { + "code": "H87", + "name": "штук" + }, + "minValue": "1" + } + ], + "id": "655360-0006-001", + "description": "Кількість портів HDMI, не менше 1 шт." + } + ], + "title": "Кількість портів HDMI", + "description": "Кількість портів HDMI", + "id": "655360-0006" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "integer", + "title": "Кількість портів D-sub", + "id": "655360-0007-001-01", + "unit": { + "code": "H87", + "name": "штук" + }, + "minValue": "1" + } + ], + "id": "655360-0007-001", + "description": "Кількість портів D-sub, не менше 1 шт." + } + ], + "title": "Кількість портів D-sub", + "description": "Кількість портів D-sub", + "id": "655360-0007" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "string", + "id": "655360-0008-001-01", + "expectedValue": "HDMI", + "title": "Кабель для під’єднання" + } + ], + "id": "655360-0008-001", + "description": "Кабель для під’єднання" + } + ], + "title": "Кабель для під’єднання", + "description": "Кабель для під’єднання", + "id": "655360-0008" + }, + { + "requirementGroups": [ + { + "requirements": [ + { + "dataType": "integer", + "title": "Гарантія", + "id": "655360-0009-001-01", + "unit": { + "code": "MON", + "name": "місяців" + }, + "minValue": "36" + } + ], + "id": "655360-0009-001", + "description": "Гарантія, не менше 36 місяців" + } + ], + "title": "Гарантія", + "description": "Строк дії гарантії", + "id": "655360-0009" + } + ], + "owner": "broker", + "next_check": "2020-05-29T01:00:00+03:00", + "awardCriteria": "lowestCost", + "id": "195eb794810a428e8ecfea8e721dc668", + "tenderID": "UA-2020-05-15-000001" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-after-bot-unsuccessful.http b/docs/source/tendering/pricequotation/http/tender-after-bot-unsuccessful.http new file mode 100644 index 0000000000..9860faee72 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-after-bot-unsuccessful.http @@ -0,0 +1,71 @@ +GET /api/2.5/tenders/ded0c35b2c4d40d8a179db607b249016 HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "draft.unsuccessful", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "startDate": "2020-05-15T01:00:00+03:00", + "endDate": "2020-05-29T01:00:00+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "description": "Комп’ютерне обладнання", + "quantity": 1.0, + "id": "e3c7cad4a9cf4ed8923771740864fbab", + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000.0, + "valueAddedTaxIncluded": true + }, + "submissionMethod": "electronicAuction", + "date": "2020-05-15T01:00:00+03:00", + "profile": "655360-30230000-889652-40000777bad_profile", + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + }, + "awardCriteria": "lowestCost", + "owner": "broker", + "dateModified": "2020-05-15T01:00:00+03:00", + "id": "ded0c35b2c4d40d8a179db607b249016", + "tenderID": "UA-2020-05-15-000002" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-get-contract-value.http b/docs/source/tendering/pricequotation/http/tender-contract-get-contract-value.http new file mode 100644 index 0000000000..01e5885b95 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-get-contract-value.http @@ -0,0 +1,71 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56 HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "value": { + "currency": "UAH", + "amount": 479.0, + "amountNet": 479.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "awardID": "c58f6124caae4fa8aee6e33233bb4ba3", + "id": "081c725a9a7f49c08e30ff870b3c2d56", + "contractID": "UA-2020-05-15-000001-2" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-get-documents-again.http b/docs/source/tendering/pricequotation/http/tender-contract-get-documents-again.http new file mode 100644 index 0000000000..13442c5bcb --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-get-documents-again.http @@ -0,0 +1,31 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56/documents HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": [ + { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_second_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/081deb42a20d45d299cea86a0dd80346?KeyID=a8968c46&Signature=Z1jfOvOOx0D1%252BMAWySO1THwhLB7v2WglDYjZtfyLdka3zO3hir%2FtiKJuEVHownh%252BAbqRz2fZAJ6ICpkEmuDABQ%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "58b01bbf809148a9a155be0fb7dc7e5f", + "dateModified": "2020-05-15T01:00:03+03:00" + }, + { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_first_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/05c283398a464e0fa58038d9e1313f94?KeyID=a8968c46&Signature=XdzOWMLCaq8jRpC%2FI4N7%2Fb6Ec5l39pRbOpmyVKtiBYl140v4uJzfifxecIIdxnCggyD4GBWsZCIXFEZxGyUaAg%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "dc5e406cc06d44659918ed3acb29a6bc", + "dateModified": "2020-05-15T01:00:03+03:00" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-get-documents.http b/docs/source/tendering/pricequotation/http/tender-contract-get-documents.http new file mode 100644 index 0000000000..6934422047 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-get-documents.http @@ -0,0 +1,21 @@ +GET /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56/documents HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": [ + { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_first_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/05c283398a464e0fa58038d9e1313f94?KeyID=a8968c46&Signature=XdzOWMLCaq8jRpC%2FI4N7%2Fb6Ec5l39pRbOpmyVKtiBYl140v4uJzfifxecIIdxnCggyD4GBWsZCIXFEZxGyUaAg%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "dc5e406cc06d44659918ed3acb29a6bc", + "dateModified": "2020-05-15T01:00:03+03:00" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-period.http b/docs/source/tendering/pricequotation/http/tender-contract-period.http new file mode 100644 index 0000000000..735d209d21 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-period.http @@ -0,0 +1,88 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 104 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "period": { + "startDate": "2020-05-15T01:00:03+03:00", + "endDate": "2021-05-15T01:00:03+03:00" + } + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "contractNumber": "contract #13111", + "period": { + "startDate": "2020-05-15T01:00:03+03:00", + "endDate": "2021-05-15T01:00:03+03:00" + }, + "dateSigned": "2020-05-15T01:00:03+03:00", + "value": { + "currency": "UAH", + "amount": 238.0, + "amountNet": 230.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "awardID": "c58f6124caae4fa8aee6e33233bb4ba3", + "id": "081c725a9a7f49c08e30ff870b3c2d56", + "contractID": "UA-2020-05-15-000001-2" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-set-contract-value.http b/docs/source/tendering/pricequotation/http/tender-contract-set-contract-value.http new file mode 100644 index 0000000000..1ec5a29919 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-set-contract-value.http @@ -0,0 +1,84 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 91 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "contractNumber": "contract #13111", + "value": { + "amount": 238, + "amountNet": 230 + } + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "pending", + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "contractNumber": "contract #13111", + "value": { + "currency": "UAH", + "amount": 238.0, + "amountNet": 230.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:01+03:00", + "awardID": "c58f6124caae4fa8aee6e33233bb4ba3", + "id": "081c725a9a7f49c08e30ff870b3c2d56", + "contractID": "UA-2020-05-15-000001-2" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-sign-date.http b/docs/source/tendering/pricequotation/http/tender-contract-sign-date.http new file mode 100644 index 0000000000..1ef201bb77 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-sign-date.http @@ -0,0 +1,16 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 53 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "dateSigned": "2020-05-15T01:00:03+03:00" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +null + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-sign.http b/docs/source/tendering/pricequotation/http/tender-contract-sign.http new file mode 100644 index 0000000000..4126185636 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-sign.http @@ -0,0 +1,107 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 30 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "status": "active" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "active", + "documents": [ + { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_first_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/05c283398a464e0fa58038d9e1313f94?KeyID=a8968c46&Signature=XdzOWMLCaq8jRpC%2FI4N7%2Fb6Ec5l39pRbOpmyVKtiBYl140v4uJzfifxecIIdxnCggyD4GBWsZCIXFEZxGyUaAg%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "dc5e406cc06d44659918ed3acb29a6bc", + "dateModified": "2020-05-15T01:00:03+03:00" + }, + { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_second_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/081deb42a20d45d299cea86a0dd80346?KeyID=a8968c46&Signature=Z1jfOvOOx0D1%252BMAWySO1THwhLB7v2WglDYjZtfyLdka3zO3hir%2FtiKJuEVHownh%252BAbqRz2fZAJ6ICpkEmuDABQ%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "58b01bbf809148a9a155be0fb7dc7e5f", + "dateModified": "2020-05-15T01:00:03+03:00" + } + ], + "items": [ + { + "description": "Комп’ютерне обладнання", + "classification": { + "scheme": "ДК021", + "description": "Комп’ютерне обладнанн", + "id": "30230000-0" + }, + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + }, + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "unit": { + "code": "H87", + "name": "штук" + }, + "quantity": 1.0 + } + ], + "suppliers": [ + { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "scale": "micro", + "name": "Державне управління справами", + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "contractNumber": "contract #13111", + "period": { + "startDate": "2020-05-15T01:00:03+03:00", + "endDate": "2021-05-15T01:00:03+03:00" + }, + "dateSigned": "2020-05-15T01:00:03+03:00", + "value": { + "currency": "UAH", + "amount": 238.0, + "amountNet": 230.0, + "valueAddedTaxIncluded": true + }, + "date": "2020-05-15T01:00:03+03:00", + "awardID": "c58f6124caae4fa8aee6e33233bb4ba3", + "id": "081c725a9a7f49c08e30ff870b3c2d56", + "contractID": "UA-2020-05-15-000001-2" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-upload-document.http b/docs/source/tendering/pricequotation/http/tender-contract-upload-document.http new file mode 100644 index 0000000000..89e32aeb5f --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-upload-document.http @@ -0,0 +1,31 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56/documents?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 342 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/05c283398a464e0fa58038d9e1313f94?KeyID=a8968c46&Signature=sMN9w%2BDHK0IGarsSHBZTUYyweUvjV5rFw5BHN8auvd8C4ALcr5vEqyKi9h3%2F5ONr2EntFooCIoUxkFKiLX0bCw%3D%3D", + "title": "contract_first_document.doc", + "hash": "md5:00000000000000000000000000000000", + "format": "application/msword" + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56/documents/dc5e406cc06d44659918ed3acb29a6bc +{ + "data": { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_first_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/05c283398a464e0fa58038d9e1313f94?KeyID=a8968c46&Signature=XdzOWMLCaq8jRpC%2FI4N7%2Fb6Ec5l39pRbOpmyVKtiBYl140v4uJzfifxecIIdxnCggyD4GBWsZCIXFEZxGyUaAg%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "dc5e406cc06d44659918ed3acb29a6bc", + "dateModified": "2020-05-15T01:00:03+03:00" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-contract-upload-second-document.http b/docs/source/tendering/pricequotation/http/tender-contract-upload-second-document.http new file mode 100644 index 0000000000..af74633413 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-contract-upload-second-document.http @@ -0,0 +1,31 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56/documents?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 345 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/081deb42a20d45d299cea86a0dd80346?KeyID=a8968c46&Signature=p8eWvzGEen2pOXnZUpYiT15%2B0%2FncoFwBkROF9jLQ4MzO5uDYK38u98XrD%2Fa8WY3sc8nvuuLXIHOgJ2i1GPUICQ%3D%3D", + "title": "contract_second_document.doc", + "hash": "md5:00000000000000000000000000000000", + "format": "application/msword" + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/contracts/081c725a9a7f49c08e30ff870b3c2d56/documents/58b01bbf809148a9a155be0fb7dc7e5f +{ + "data": { + "hash": "md5:00000000000000000000000000000000", + "title": "contract_second_document.doc", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/081deb42a20d45d299cea86a0dd80346?KeyID=a8968c46&Signature=Z1jfOvOOx0D1%252BMAWySO1THwhLB7v2WglDYjZtfyLdka3zO3hir%2FtiKJuEVHownh%252BAbqRz2fZAJ6ICpkEmuDABQ%253D%253D", + "format": "application/msword", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "58b01bbf809148a9a155be0fb7dc7e5f", + "dateModified": "2020-05-15T01:00:03+03:00" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-listing-after-creation.http b/docs/source/tendering/pricequotation/http/tender-listing-after-creation.http new file mode 100644 index 0000000000..a5d2567b5e --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-listing-after-creation.http @@ -0,0 +1,15 @@ +GET /api/2.5/tenders HTTP/1.0 +Authorization: Bearer broker +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "next_page": { + "path": "/api/2.5/tenders?offset=", + "uri": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders?offset=", + "offset": "" + }, + "data": [] +} + diff --git a/docs/source/tendering/pricequotation/http/tender-listing-after-patch.http b/docs/source/tendering/pricequotation/http/tender-listing-after-patch.http new file mode 100644 index 0000000000..3a7e704990 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-listing-after-patch.http @@ -0,0 +1,19 @@ +GET /api/2.5/tenders?opt_pretty=1 HTTP/1.0 +Host: lb-api-sandbox.prozorro.gov.ua + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "next_page": { + "path": "/api/2.5/tenders?offset=2020-05-15T01%3A00%3A01%2B03%3A00", + "uri": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders?offset=2020-05-15T01%3A00%3A01%2B03%3A00", + "offset": "2020-05-15T01:00:01+03:00" + }, + "data": [ + { + "id": "2f6b50d7e72d4fc184f561d945af0397", + "dateModified": "2020-05-15T01:00:01+03:00" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/tender-post-attempt-json-data.http b/docs/source/tendering/pricequotation/http/tender-post-attempt-json-data.http new file mode 100644 index 0000000000..fa1c2705d6 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-post-attempt-json-data.http @@ -0,0 +1,132 @@ +POST /api/2.5/tenders?opt_pretty=1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 1802 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "profile": "655360-30230000-889652-40000777", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "endDate": "2020-05-29T01:00:00+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "quantity": 1, + "description": "Комп’ютерне обладнання", + "deliveryAddress": { + "countryName": "Україна", + "postalCode": "79000", + "region": "м. Київ", + "streetAddress": "вул. Банкова 1", + "locality": "м. Київ" + } + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000 + }, + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "countryName": "Україна", + "postalCode": "01220", + "region": "м. Київ", + "streetAddress": "вул. Банкова, 11, корпус 1", + "locality": "м. Київ" + } + } + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397 +{ + "access": { + "transfer": "d085ec4136e0420ba245de7739627028", + "token": "ac342a9f314046808ffa89602219bf8d" + }, + "data": { + "status": "draft", + "procurementMethod": "selective", + "mainProcurementCategory": "goods", + "tenderPeriod": { + "startDate": "2020-05-15T01:00:00+03:00", + "endDate": "2020-05-29T01:00:00+03:00" + }, + "title": "Комп’ютерне обладнання", + "items": [ + { + "deliveryDate": { + "startDate": "2020-05-17T01:00:00+03:00", + "endDate": "2020-05-20T01:00:00+03:00" + }, + "description": "Комп’ютерне обладнання", + "quantity": 1.0, + "id": "d2c4f983cbf340e29ff714a0f99d8844", + "deliveryAddress": { + "postalCode": "79000", + "countryName": "Україна", + "streetAddress": "вул. Банкова 1", + "region": "м. Київ", + "locality": "м. Київ" + } + } + ], + "procurementMethodType": "priceQuotation", + "value": { + "currency": "UAH", + "amount": 22000.0, + "valueAddedTaxIncluded": true + }, + "submissionMethod": "electronicAuction", + "date": "2020-05-15T01:00:00+03:00", + "profile": "655360-30230000-889652-40000777", + "procuringEntity": { + "contactPoint": { + "name": "Державне управління справами", + "telephone": "0440000000" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00037256", + "uri": "http://www.dus.gov.ua/" + }, + "name": "Державне управління справами", + "kind": "general", + "address": { + "postalCode": "01220", + "countryName": "Україна", + "streetAddress": "вул. Банкова, 11, корпус 1", + "region": "м. Київ", + "locality": "м. Київ" + } + }, + "awardCriteria": "lowestCost", + "owner": "broker", + "dateModified": "2020-05-15T01:00:00+03:00", + "id": "2f6b50d7e72d4fc184f561d945af0397", + "tenderID": "UA-2020-05-15-000001" + } +} + diff --git a/docs/source/tendering/pricequotation/http/tender-post-attempt-json.http b/docs/source/tendering/pricequotation/http/tender-post-attempt-json.http new file mode 100644 index 0000000000..ce2f626d6a --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-post-attempt-json.http @@ -0,0 +1,21 @@ +POST /api/2.5/tenders?opt_pretty=1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 4 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +data + +Response: 422 Unprocessable Entity +Content-Type: application/json; charset=UTF-8 +{ + "status": "error", + "errors": [ + { + "description": "No JSON object could be decoded", + "location": "body", + "name": "data" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/tender-post-attempt.http b/docs/source/tendering/pricequotation/http/tender-post-attempt.http new file mode 100644 index 0000000000..ecb8be2c7f --- /dev/null +++ b/docs/source/tendering/pricequotation/http/tender-post-attempt.http @@ -0,0 +1,21 @@ +POST /api/2.5/tenders?opt_pretty=1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 4 +Content-Type: application/x-www-form-urlencoded +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +data + +Response: 415 Unsupported Media Type +Content-Type: application/json; charset=UTF-8 +{ + "status": "error", + "errors": [ + { + "description": "Content-Type header should be one of ['application/json']", + "location": "header", + "name": "Content-Type" + } + ] +} + diff --git a/docs/source/tendering/pricequotation/http/update-cancellation-doc.http b/docs/source/tendering/pricequotation/http/update-cancellation-doc.http new file mode 100644 index 0000000000..2edf225a50 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/update-cancellation-doc.http @@ -0,0 +1,31 @@ +PUT /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d/documents/8eab55698e214da9a8596071cc90ed8c?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 324 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/4160874d71844a7c987fc08322d8b775?KeyID=a8968c46&Signature=nQwYbLj8cKUOdMU%2FIpht86Zd1tGEjzKwOkcubC2lGlswdV%2Bf5W9igwtfpOjWfKuMZQMCa1AyHv8DyzopLPwSAg%3D%3D", + "title": "Notice-2.pdf", + "hash": "md5:00000000000000000000000000000000", + "format": "application/pdf" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "hash": "md5:00000000000000000000000000000000", + "description": "Changed description", + "title": "Notice-2.pdf", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/4160874d71844a7c987fc08322d8b775?KeyID=a8968c46&Signature=d3ac3FsDW4CfeyBeKPkEuzY0tNdHnRpx%252BW%2FdGnY961d%252BwKR2ctPXMdR%252BYgWVBFcX5TcVrvIIpXTorapeIFdjCA%253D%253D", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "8eab55698e214da9a8596071cc90ed8c", + "dateModified": "2020-05-15T01:00:03+03:00" + } +} + diff --git a/docs/source/tendering/pricequotation/http/update-cancellation-reasonType.http b/docs/source/tendering/pricequotation/http/update-cancellation-reasonType.http new file mode 100644 index 0000000000..665794ec1a --- /dev/null +++ b/docs/source/tendering/pricequotation/http/update-cancellation-reasonType.http @@ -0,0 +1,25 @@ +PATCH /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 39 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "reasonType": "expensesCut" + } +} + +Response: 200 OK +Content-Type: application/json; charset=UTF-8 +{ + "data": { + "status": "draft", + "reason": "cancellation reason", + "reasonType": "expensesCut", + "date": "2020-05-15T01:00:03+03:00", + "cancellationOf": "tender", + "id": "4b9cfb25db254d3ab12593f969cfc64d" + } +} + diff --git a/docs/source/tendering/pricequotation/http/upload-bid-proposal.http b/docs/source/tendering/pricequotation/http/upload-bid-proposal.http new file mode 100644 index 0000000000..f09a64237b --- /dev/null +++ b/docs/source/tendering/pricequotation/http/upload-bid-proposal.http @@ -0,0 +1,31 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272/documents?acc_token=7ee1f0a7d9ca4a6d95fe2bf79100a9e1 HTTP/1.0 +Authorization: Bearer broker +Content-Length: 328 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/fb1bfc41e45e4a6c946adbd59bfdff16?KeyID=a8968c46&Signature=mq3jIE1hdYFYj9aMeyhNsq6%2BhLzC9iYNKM%2BhkWN89llAF5r%2ByQr4qxtwrCv8V21AmZVUajRa3yCLg7Qc%2BVC9DQ%3D%3D", + "title": "Proposal.pdf", + "hash": "md5:00000000000000000000000000000000", + "format": "application/pdf" + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272/documents/6f3bb464ac7844b7ac3136550e8751e4 +{ + "data": { + "hash": "md5:00000000000000000000000000000000", + "title": "Proposal.pdf", + "url": "http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/bids/e46b68ebf7724fcfa182eeade05ec272/documents/6f3bb464ac7844b7ac3136550e8751e4?download=fb1bfc41e45e4a6c946adbd59bfdff16", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:01+03:00", + "id": "6f3bb464ac7844b7ac3136550e8751e4", + "dateModified": "2020-05-15T01:00:01+03:00" + } +} + diff --git a/docs/source/tendering/pricequotation/http/upload-cancellation-doc.http b/docs/source/tendering/pricequotation/http/upload-cancellation-doc.http new file mode 100644 index 0000000000..ba0bb29340 --- /dev/null +++ b/docs/source/tendering/pricequotation/http/upload-cancellation-doc.http @@ -0,0 +1,31 @@ +POST /api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d/documents?acc_token=ac342a9f314046808ffa89602219bf8d HTTP/1.0 +Authorization: Bearer broker +Content-Length: 322 +Content-Type: application/json +Host: lb-api-sandbox.prozorro.gov.ua +DATA: +{ + "data": { + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/6c09ba3998ad4cb8a1232d732d0b418a?KeyID=a8968c46&Signature=l0wRyBIXGKOFEiYj%2FEJncxnNqnBfZgz2hQ6Ppbv%2BB3L7Pzu7O7vKKTmYoDO3zrUm8S69EbxfWiqmVNvFsG5PAg%3D%3D", + "title": "Notice.pdf", + "hash": "md5:00000000000000000000000000000000", + "format": "application/pdf" + } +} + +Response: 201 Created +Content-Type: application/json; charset=UTF-8 +Location: http://lb-api-sandbox.prozorro.gov.ua/api/2.5/tenders/2f6b50d7e72d4fc184f561d945af0397/cancellations/4b9cfb25db254d3ab12593f969cfc64d/documents/8eab55698e214da9a8596071cc90ed8c +{ + "data": { + "hash": "md5:00000000000000000000000000000000", + "title": "Notice.pdf", + "url": "http://public-docs-sandbox.prozorro.gov.ua/get/6c09ba3998ad4cb8a1232d732d0b418a?KeyID=a8968c46&Signature=1kVtsM7NZS1ALmYHAnefygkvEQVOcy%2FvAWxtCk5HXRMuOKyfXo23XH8yrDAbKuFWAAJWkm1HYYgW7kPVMTSrDg%253D%253D", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2020-05-15T01:00:03+03:00", + "id": "8eab55698e214da9a8596071cc90ed8c", + "dateModified": "2020-05-15T01:00:03+03:00" + } +} + diff --git a/docs/source/tendering/pricequotation/index.rst b/docs/source/tendering/pricequotation/index.rst new file mode 100644 index 0000000000..ace90d3116 --- /dev/null +++ b/docs/source/tendering/pricequotation/index.rst @@ -0,0 +1,12 @@ +.. _pricequotation: + +Price Quotation procedure +============================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + overview + tutorial diff --git a/docs/source/tendering/pricequotation/overview.rst b/docs/source/tendering/pricequotation/overview.rst new file mode 100644 index 0000000000..042400b3ec --- /dev/null +++ b/docs/source/tendering/pricequotation/overview.rst @@ -0,0 +1,37 @@ +.. _pricequotation_overview: + +Overview +======== + +The Open Procurement `Price Quotation` procedure is plugin to `Open Procurement API` software. + +REST-ful interface to plugin is in line with core software design principles. + +Main responsibilities +--------------------- + +Price Quotation procedure is dedicated to Open Tender procedure for Ukrainian below threshold procurements. The code for that type of procedure is `priceQuotation`. + +Business logic +-------------- + +1) Publication of the Price Quotation + +Business process begins when the Procuring Entity creates a Price Quotation procedure using parameters from the e-Catalogues Profile database. + +After Procuring Entity supplements the procedure with quantity of items and delivery details and publishes the tender by sending a request for Price Quotation to ProZorro Business Process Engine the process starts. + +At this moment Business Process Engine receives and validates the Price Quotation request. Given the validation is passed the system automatically informs shortlisted (qualified to specific eCatalogue Profile) suppliers about the request. + +2) Tendering + +Receiving a Price Quotation request, supplier decides if they are able to offer the requested product. In case of rejection supplier declines participation in procedure. +Until the end of tender period (minimal two working days) suppliers would be able to submit a bid, while BPE will collect and register quotations. + +3) Awarding, Qualification + +After the deadline system will publish received bids, awarding suppleir with most economically advantageous bid allowing to confirm award within two business days. In case if award was not confirmed system will automatically award next supplier providing same confirmation period. In case of no suppliers left system will transfer procedure to status `unsuccessful`. + +4) Contracting + +Selecting a winner will lead both Procuring Entity and supplier to the contracting process, where the contract is signed, published and taken to execution. diff --git a/docs/source/tendering/pricequotation/tutorial.rst b/docs/source/tendering/pricequotation/tutorial.rst new file mode 100644 index 0000000000..aee18138b6 --- /dev/null +++ b/docs/source/tendering/pricequotation/tutorial.rst @@ -0,0 +1,289 @@ +.. _pricequotation_tutorial: + +Tutorial +======== + +Exploring basic rules +--------------------- + +Let's try exploring the `/tenders` endpoint: + + +.. include:: http/initial-tender-listing.http + :code: + +Just invoking it reveals empty set. + +Now let's attempt creating some tender: + +.. include:: http/tender-post-attempt.http + :code: + +Error states that the only accepted Content-Type is `application/json`. + +Let's satisfy the Content-type requirement: + +.. include:: http/tender-post-attempt-json.http + :code: + +Error states that no `data` has been found in JSON body. + + +.. index:: Tender + +Creating tender +--------------- + +Let's provide the data attribute in the submitted body : + +.. include:: http/tender-post-attempt-json-data.http + :code: + +Success! Now we can see that new object was created. Response code is `201` +and `Location` response header reports the location of the created object. The +body of response reveals the information about the created tender: its internal +`id` (that matches the `Location` segment), its official `tenderID` and +`dateModified` datestamp stating the moment in time when tender was last +modified. Note that tender is created with `draft` status. + +**! Note:** User receives `access`: `token`: ``"151a30932ee245e989771be867bc8235"`` with which operations as a `Procuring Entity` role are accessible. + +Price Quotation procedure has ``procurementMethodType``: ``priceQuotation`` and ``procurementMethod``: ``selective``. + +Let's access the URL of the created object (the `Location` header of the response): + +.. include:: http/blank-tender-view.http + :code: + +We can see the same response we got after creating tender. + +Modifying tender +---------------- + +Procuring Entity can modify tender before publishing. +Let's update tender by supplementing it with all other essential properties: + +.. include:: http/patch-tender-data.http + :code: + +We see the added properties have merged with existing tender data. Additionally, the `dateModified` property was updated to reflect the last modification datestamp. + +Checking the listing again reflects the new modification date: + +.. include:: http/tender-listing-after-patch.http + :code: + +Publishing tender +------------------ + +After creation Procuring Entity publishes procedure by changing status to `draft.publishing` where **priceQuotationBot** robot runs validation of the procedure and supplement procedure with additional data taken from ProZorro e-Catalogues database including `shortListedFirms`. + +.. include:: http/publish-tender.http + :code: + + +After successful validation priceQuotationBot transmit procedure to status: `active.tendering` + +.. include:: http/tender-after-bot-active.http + :code: + +In case if procedure do not pass validation due to invalid options, it will be switched to status: `draft.unsuccessful` by the **priceQuotationBot**. + +.. include:: http/tender-after-bot-unsuccessful.http + :code: + +.. index:: Document + +Bid submission +-------------- + +Registering bid +~~~~~~~~~~~~~~~ +Tender status ``active.tendering`` allows registration of bids. + +Bidder can register a bid with ``draft`` status: + +.. include:: http/register-bidder.http + :code: + +**! Note:** User receives `access`: `token`: ``"00e173e5f31f4decbb811cc01e10c1bf"`` with which operations as a `Supplier` role are accessible. + + +And activate a bid: + +.. include:: http/activate-bidder.http + :code: + +Modifying bid +~~~~~~~~~~~~~~~ + +Bid can be updated until the end of tender period. + +.. include:: http/patch-bidder.http + :code: + +Proposal Uploading +~~~~~~~~~~~~~~~~~~ + +Then bidder should upload proposal document(s): + +.. include:: http/upload-bid-proposal.http + :code: + +It is possible to check the uploaded documents: + +.. include:: http/bidder-documents.http + :code: + +.. index:: Awarding + +Awarding process +---------------- + +After the tender period end date, system automatically creates `award` in `pending` status for the bid with the most economically advantageous price. + +.. include:: http/awards-listing.http + :code: + +The Supplier-winner can accept `award` by transferring it to status: `active`. The system is waiting for acceptance from the supplier-winner within `two working days`. + +.. include:: http/award-active.http + :code: + +Procuring Entity can cancel `award` after acceptance by changing `award` status to `cancelled` in case if supplier-winner declines to sign contract. + +.. include:: http/award-cancelled.http + :code: + +After canceling `award` system creates `second` `award` for the same bid in status: `pending` with access for Procuring Entity only. +By the decision of Procuring Entity `second` `award` can be either changed for `active` or to `unsuccessful` with ability to upload supplementary documents. + +The Supplier-winner can decline `award` by transferring it to status: `unsuccessful`. + +.. include:: http/award-unsuccesful.http + :code: + +`Award` will be granted to the next bid with most economically advantageous price, for the following cases: + + 1. Supplier-winner didn't accept `award` within two working days. + 2. Supplier-winner declined `award`. + 3. Supplier-winner refused to sign contract and `award` was canceled by Procuring Entity. + +**Note !** In the case of `award` being transferred to `unsuccessful` status for the last bid, procedure will inherit termination status: **`unsuccessful`**. + +.. index:: Setting Contract + +Setting contract +---------------- + +Setting contract value +~~~~~~~~~~~~~~~~~~~~~~ + +By default contract value is set based on the award, but there is a possibility to set custom contract value. + +If you want to **lower contract value**, you can insert new one into the `amount` field. + +.. include:: http/tender-contract-set-contract-value.http + :code: + +`200 OK` response was returned. The value was modified successfully. + +Setting contract signature date +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is a possibility to set custom contract signature date. You can insert appropriate date into the `dateSigned` field. + +If this date is not set, it will be auto-generated on the date of contract registration. + +.. include:: http/tender-contract-sign-date.http + :code: + +Setting contract validity period +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting contract validity period is optional, but if it is needed, you can set appropriate `startDate` and `endDate`. + +.. include:: http/tender-contract-period.http + :code: + +Uploading contract documentation +-------------------------------- + +You can upload contract documents for the Price Quotation procedure. + +Let's upload contract document: + +.. include:: http/tender-contract-upload-document.http + :code: + +`201 Created` response code and `Location` header confirm that this document was added. + +Let's view the uploaded contract document: + +.. include:: http/tender-contract-get-documents.http + :code: + +Cancelling tender +----------------- + +Tender creator can cancel tender anytime (except when tender in terminal status e.g. `draft.unsuccessful`, `unsuccessful`, `cancelled`, `complete`). + +The following steps should be applied: + +1. Prepare cancellation request. +2. Fill it with the protocol describing the cancellation reasons. +3. Cancel the tender with the prepared reasons. + +Only the request that has been activated (3rd step above) has power to +cancel tender. I.e. you have to not only prepare cancellation request but +to activate it as well. + +For cancelled cancellation you need to update cancellation status to `unsuccessful` +from `draft` or `pending`. + +See :ref:`cancellation` data structure for details. + +Preparing the cancellation request +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You should pass `reason` and `reasonType`, `status` defaults to `draft`. + +There are four possible types of cancellation reason - tender was `noDemand`, `unFixable`, `forceMajeure` and `expensesCut`. + +`id` is autogenerated and passed in the `Location` header of response. + +.. include:: http/prepare-cancellation.http + :code: + +You can change ``reasonType`` value to any of the above. + +.. include:: http/update-cancellation-reasonType.http + :code: + +Filling cancellation with protocol and supplementary documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This step is required. Without documents you can't update tender status. + +Upload the file contents + +.. include:: http/upload-cancellation-doc.http + :code: + +Change the document description and other properties + + +.. include:: http/patch-cancellation.http + :code: + +Upload new version of the document + + +.. include:: http/update-cancellation-doc.http + :code: + +Activating the request and cancelling tender +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: http/active-cancellation.http + :code: diff --git a/docs/tests/auth.ini b/docs/tests/auth.ini index 9dd2e996e5..b5839f69d2 100644 --- a/docs/tests/auth.ini +++ b/docs/tests/auth.ini @@ -33,6 +33,7 @@ test = token [bots] bot = bot +pricequotation = pricequotation [contracting] contracting = contracting diff --git a/docs/tests/test_pricequotation.py b/docs/tests/test_pricequotation.py new file mode 100644 index 0000000000..4bbd3aa81e --- /dev/null +++ b/docs/tests/test_pricequotation.py @@ -0,0 +1,467 @@ +# -*- coding: utf-8 -*- +import os +from copy import deepcopy +from uuid import uuid4 +from datetime import timedelta + +from openprocurement.api.models import get_now +from openprocurement.tender.pricequotation.tests.base import ( + BaseTenderWebTest, test_tender_data, test_bids, bid_with_docs, test_short_profile, + test_shortlisted_firms) +from openprocurement.tender.core.tests.base import change_auth + +from tests.base.test import DumpsWebTestApp, MockWebTestMixin +from tests.base.constants import DOCS_URL, AUCTIONS_URL + +test_tender_data = deepcopy(test_tender_data) +test_tender_data['items'][0].pop('classification') +test_tender_data['items'][0].pop('additionalClassifications') +bid_draft = deepcopy(test_bids[0]) +bid_draft["status"] = "draft" + +TARGET_DIR = 'docs/source/tendering/pricequotation/http/' + + +class TenderResourceTest(BaseTenderWebTest, MockWebTestMixin): + AppClass = DumpsWebTestApp + + relative_to = os.path.dirname(__file__) + initial_data = test_tender_data + initial_bids = test_bids + docservice = True + docservice_url = DOCS_URL + auctions_url = AUCTIONS_URL + + def setUp(self): + super(TenderResourceTest, self).setUp() + self.setUpMock() + + def tearDown(self): + self.tearDownMock() + super(TenderResourceTest, self).tearDown() + + def test_docs_publish_tenders(self): + tender_data = deepcopy(test_tender_data) + tender_data.update({ + "tenderPeriod": {"endDate": (get_now() + timedelta(days=14)).isoformat()} + }) + for item in tender_data['items']: + item['deliveryDate'] = { + "startDate": (get_now() + timedelta(days=2)).isoformat(), + "endDate": (get_now() + timedelta(days=5)).isoformat() + } + + tender_data_1 = deepcopy(tender_data) + tender_data_1['profile'] = test_short_profile["id"] + response = self.app.post_json("/tenders", {"data": tender_data_1}) + self.assertEqual(response.status, "201 Created") + tender_id_1 = response.json["data"]["id"] + owner_token = response.json["access"]["token"] + tender = response.json["data"] + + self.assertEqual(tender["status"], "draft") + self.assertEqual(len(tender["items"]), 1) + self.assertNotIn("shortlistedFirms", tender) + self.assertNotIn("classification", tender["items"][0]) + self.assertNotIn("unit", tender["items"][0]) + self.assertEqual(tender["profile"], test_short_profile["id"]) + + with open(TARGET_DIR + 'publish-tender.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender_id_1, owner_token), + {"data": {"status": "draft.publishing"}} + ) + self.assertEqual(response.status, "200 OK") + tender = response.json["data"] + self.assertEqual(tender["status"], "draft.publishing") + + items = deepcopy(tender["items"]) + items[0]["classification"] = test_short_profile["classification"] + items[0]["unit"] = test_short_profile["unit"] + criteria = deepcopy(test_short_profile["criteria"]) + data = { + "data": { + "status": "active.tendering", + "items": items, + "shortlistedFirms": test_shortlisted_firms, + "criteria": criteria + } + } + + test_tender_data2 = deepcopy(tender_data) + test_tender_data2["profile"] += "bad_profile" + + response = self.app.post_json( + '/tenders?opt_pretty=1', + {'data': test_tender_data2}) + self.assertEqual(response.status, '201 Created') + + tender_id_2 = response.json['data']['id'] + + with change_auth(self.app, ("Basic", ("pricequotation", ""))) as app: + resp = app.patch_json("/tenders/{}".format(tender_id_1), data) + self.assertEqual(resp.status, "200 OK") + resp = app.patch_json('/tenders/{}'.format(tender_id_2), {"data": {"status": "draft.unsuccessful"}}) + self.assertEqual(resp.status, "200 OK") + + with open(TARGET_DIR + 'tender-after-bot-active.http', 'w') as self.app.file_obj: + response = self.app.get("/tenders/{}".format(tender_id_1)) + tender = response.json["data"] + self.assertEqual(response.status, "200 OK") + self.assertIn("shortlistedFirms", tender) + self.assertIn("classification", tender["items"][0]) + self.assertIn("unit", tender["items"][0]) + + with open(TARGET_DIR + 'tender-after-bot-unsuccessful.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}'.format(tender_id_2)) + self.assertEqual(response.status, '200 OK') + + def test_docs_tutorial(self): + + request_path = '/tenders?opt_pretty=1' + + # Exploring basic rules + + with open(TARGET_DIR + 'tender-post-attempt.http', 'w') as self.app.file_obj: + response = self.app.post(request_path, 'data', status=415) + self.assertEqual(response.status, '415 Unsupported Media Type') + + self.app.authorization = ('Basic', ('broker', '')) + + with open(TARGET_DIR + 'tender-post-attempt-json.http', 'w') as self.app.file_obj: + self.app.authorization = ('Basic', ('broker', '')) + response = self.app.post( + request_path, 'data', content_type='application/json', status=422) + self.assertEqual(response.status, '422 Unprocessable Entity') + + # Creating tender + + for item in test_tender_data['items']: + item['deliveryDate'] = { + "startDate": (get_now() + timedelta(days=2)).isoformat(), + "endDate": (get_now() + timedelta(days=5)).isoformat() + } + + test_tender_data.update({ + "tenderPeriod": {"endDate": (get_now() + timedelta(days=14)).isoformat()} + }) + + with open(TARGET_DIR + 'tender-post-attempt-json-data.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders?opt_pretty=1', + {'data': test_tender_data}) + self.assertEqual(response.status, '201 Created') + + tender = response.json['data'] + owner_token = response.json['access']['token'] + self.tender_id = tender['id'] + + with open(TARGET_DIR + 'blank-tender-view.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}'.format(tender['id'])) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'initial-tender-listing.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders') + self.assertEqual(response.status, '200 OK') + + response = self.app.post_json( + '/tenders?opt_pretty=1', + {'data': test_tender_data}) + self.assertEqual(response.status, '201 Created') + + with open(TARGET_DIR + 'tender-listing-after-creation.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders') + self.assertEqual(response.status, '200 OK') + + self.app.authorization = ('Basic', ('broker', '')) + + # Modifying tender + + self.tick() + + tenderPeriod_endDate = get_now() + timedelta(days=15, seconds=10) + with open(TARGET_DIR + 'patch-tender-data.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}?acc_token={}'.format(tender['id'], owner_token), + {'data': {"tenderPeriod": {"endDate": tenderPeriod_endDate.isoformat()}}}) + + self.app.authorization = ('Basic', ('broker', '')) + + self.set_status('active.tendering') + + with open(TARGET_DIR + 'tender-listing-after-patch.http', 'w') as self.app.file_obj: + self.app.authorization = None + response = self.app.get(request_path) + self.assertEqual(response.status, '200 OK') + + # Registering bid + + self.app.authorization = ('Basic', ('broker', '')) + bids_access = {} + with open(TARGET_DIR + 'register-bidder.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders/{}/bids'.format(self.tender_id), + {'data': bid_draft}) + bid1_id = response.json['data']['id'] + bids_access[bid1_id] = response.json['access']['token'] + self.assertEqual(response.status, '201 Created') + + with open(TARGET_DIR + 'patch-bidder.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/bids/{}?acc_token={}'.format( + self.tender_id, bid1_id, bids_access[bid1_id] + ), + {'data': {"value": {"amount": 459}}} + ) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'activate-bidder.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/bids/{}?acc_token={}'.format( + self.tender_id, bid1_id, bids_access[bid1_id]), + {'data': {"status": "active"}}) + self.assertEqual(response.status, '200 OK') + + # Proposal Uploading + + with open(TARGET_DIR + 'upload-bid-proposal.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders/{}/bids/{}/documents?acc_token={}'.format( + self.tender_id, bid1_id, bids_access[bid1_id]), + {'data': { + 'title': u'Proposal.pdf', + 'url': self.generate_docservice_url(), + 'hash': 'md5:' + '0' * 32, + 'format': 'application/pdf', + }}) + self.assertEqual(response.status, '201 Created') + + with open(TARGET_DIR + 'bidder-documents.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/bids/{}/documents?acc_token={}'.format( + self.tender_id, bid1_id, bids_access[bid1_id])) + self.assertEqual(response.status, '200 OK') + + # Second bid registration with documents + + with open(TARGET_DIR + 'register-2nd-bidder.http', 'w') as self.app.file_obj: + for document in bid_with_docs['documents']: + document['url'] = self.generate_docservice_url() + response = self.app.post_json( + '/tenders/{}/bids'.format(self.tender_id), + {'data': bid_with_docs}) + bid2_id = response.json['data']['id'] + bids_access[bid2_id] = response.json['access']['token'] + self.assertEqual(response.status, '201 Created') + + self.set_status('active.qualification') + + with open(TARGET_DIR + 'awards-listing.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/awards'.format(self.tender_id)) + self.assertEqual(response.status, '200 OK') + + # get pending award + award = [i for i in response.json['data'] if i['status'] == 'pending'][0] + award_id = award['id'] + award_token = bids_access[award['bid_id']] + + with open(TARGET_DIR + 'award-unsuccesful.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/awards/{}?acc_token={}'.format(self.tender_id, award_id, award_token), + {"data": {"status": "unsuccessful"}}) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'awards-listing-after-cancel.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/awards'.format(self.tender_id)) + self.assertEqual(response.status, '200 OK') + award = [i for i in response.json['data'] if i['status'] == 'pending'][0] + award_id = award['id'] + award_token = bids_access[award['bid_id']] + + with open(TARGET_DIR + 'award-active.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/awards/{}?acc_token={}'.format(self.tender_id, award_id, award_token), + {"data": {"status": "active"}}) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'award-cancelled.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/awards/{}?acc_token={}'.format(self.tender_id, award_id, owner_token), + {"data": {"status": "cancelled"}}) + self.assertEqual(response.status, '200 OK') + + response = self.app.get('/tenders/{}/awards'.format(self.tender_id)) + award = [i for i in response.json['data'] if i['status'] == 'pending'][0] + award_id = award['id'] + + response = self.app.patch_json( + '/tenders/{}/awards/{}?acc_token={}'.format(self.tender_id, award_id, owner_token), + {"data": {"status": "active"}}) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'contract-listing.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/contracts'.format(self.tender_id)) + self.assertEqual(response.status, '200 OK') + + self.contract_id = [contract for contract in response.json['data'] if contract['status'] == 'pending'][0]['id'] + + #### Set contract value + + with open(TARGET_DIR + 'tender-contract-get-contract-value.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/contracts/{}'.format( + self.tender_id, self.contract_id)) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'tender-contract-set-contract-value.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/contracts/{}?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {"data": { + "contractNumber": "contract #13111", + "value": {"amount": 238, "amountNet": 230} + }}) + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.json['data']['value']['amount'], 238) + + #### Setting contract signature date + + self.tick() + + with open(TARGET_DIR + 'tender-contract-sign-date.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/contracts/{}?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {'data': {"dateSigned": get_now().isoformat()}}) + self.assertEqual(response.status, '200 OK') + + #### Setting contract period + + period_dates = {"period": { + "startDate": get_now().isoformat(), + "endDate": (get_now() + timedelta(days=365)).isoformat() + }} + with open(TARGET_DIR + 'tender-contract-period.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/contracts/{}?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {'data': {'period': period_dates["period"]}}) + self.assertEqual(response.status, '200 OK') + + #### Uploading contract documentation + + with open(TARGET_DIR + 'tender-contract-upload-document.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders/{}/contracts/{}/documents?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {'data': { + 'title': u'contract_first_document.doc', + 'url': self.generate_docservice_url(), + 'hash': 'md5:' + '0' * 32, + 'format': 'application/msword', + }}) + self.assertEqual(response.status, '201 Created') + + with open(TARGET_DIR + 'tender-contract-get-documents.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/contracts/{}/documents'.format( + self.tender_id, self.contract_id)) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'tender-contract-upload-second-document.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders/{}/contracts/{}/documents?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {'data': { + 'title': u'contract_second_document.doc', + 'url': self.generate_docservice_url(), + 'hash': 'md5:' + '0' * 32, + 'format': 'application/msword', + }}) + self.assertEqual(response.status, '201 Created') + + with open(TARGET_DIR + 'tender-contract-get-documents-again.http', 'w') as self.app.file_obj: + response = self.app.get('/tenders/{}/contracts/{}/documents'.format( + self.tender_id, self.contract_id)) + self.assertEqual(response.status, '200 OK') + + #### Setting contract signature date + + with open(TARGET_DIR + 'tender-contract-sign-date.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/contracts/{}?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {'data': {"dateSigned": get_now().isoformat()}}) + self.assertEqual(response.status, '200 OK') + + #### Contract signing + + with open(TARGET_DIR + 'tender-contract-sign.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/contracts/{}?acc_token={}'.format( + self.tender_id, self.contract_id, owner_token), + {'data': {'status': 'active'}}) + self.assertEqual(response.status, '200 OK') + + # Preparing the cancellation request + + self.set_status('active.awarded') + with open(TARGET_DIR + 'prepare-cancellation.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders/{}/cancellations?acc_token={}'.format( + self.tender_id, owner_token), + {'data': {'reason': 'cancellation reason', 'reasonType': 'noDemand'}}) + self.assertEqual(response.status, '201 Created') + + cancellation_id = response.json['data']['id'] + + # Changing cancellation reasonType + + with open(TARGET_DIR + 'update-cancellation-reasonType.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/cancellations/{}?acc_token={}'.format( + self.tender_id, cancellation_id, owner_token + ), + {'data': {'reasonType': 'expensesCut'}} + ) + self.assertEqual(response.status, '200 OK') + + # Filling cancellation with protocol and supplementary documentation + + with open(TARGET_DIR + 'upload-cancellation-doc.http', 'w') as self.app.file_obj: + response = self.app.post_json( + '/tenders/{}/cancellations/{}/documents?acc_token={}'.format( + self.tender_id, cancellation_id, owner_token), + {'data': { + 'title': u'Notice.pdf', + 'url': self.generate_docservice_url(), + 'hash': 'md5:' + '0' * 32, + 'format': 'application/pdf', + }}) + cancellation_doc_id = response.json['data']['id'] + self.assertEqual(response.status, '201 Created') + + with open(TARGET_DIR + 'patch-cancellation.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/cancellations/{}/documents/{}?acc_token={}'.format( + self.tender_id, cancellation_id, cancellation_doc_id, owner_token), + {'data': {"description": 'Changed description'}}) + self.assertEqual(response.status, '200 OK') + + with open(TARGET_DIR + 'update-cancellation-doc.http', 'w') as self.app.file_obj: + response = self.app.put_json( + '/tenders/{}/cancellations/{}/documents/{}?acc_token={}'.format( + self.tender_id, cancellation_id, cancellation_doc_id, owner_token), + {'data': { + 'title': u'Notice-2.pdf', + 'url': self.generate_docservice_url(), + 'hash': 'md5:' + '0' * 32, + 'format': 'application/pdf', + }}) + self.assertEqual(response.status, '200 OK') + + # Activating the request and cancelling tender + + with open(TARGET_DIR + 'active-cancellation.http', 'w') as self.app.file_obj: + response = self.app.patch_json( + '/tenders/{}/cancellations/{}?acc_token={}'.format( + self.tender_id, cancellation_id, owner_token), + {'data': {"status": "active"}}) + self.assertEqual(response.status, '200 OK') diff --git a/setup.py b/setup.py index 9b8fe7a39c..b85f81d550 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ "tender.esco = openprocurement.tender.esco.includeme:includeme", "tender.cfaua = openprocurement.tender.cfaua.includeme:includeme", "tender.cfaselectionua = openprocurement.tender.cfaselectionua.includeme:includeme", + "tender.pricequotation = openprocurement.tender.pricequotation.includeme:includeme", ], "openprocurement.agreements.core.plugins": [ "agreement.cfaua = openprocurement.agreement.cfaua.includeme:includeme" diff --git a/src/openprocurement/api/tests/auth.ini b/src/openprocurement/api/tests/auth.ini index da2796f364..2e6fa8e08f 100644 --- a/src/openprocurement/api/tests/auth.ini +++ b/src/openprocurement/api/tests/auth.ini @@ -45,6 +45,7 @@ test = token [bots] bot = bot +pricequotation = pricequotation [contracting] contracting = contracting diff --git a/src/openprocurement/api/tests/base.py b/src/openprocurement/api/tests/base.py index 38ffa15069..cd80becb2c 100644 --- a/src/openprocurement/api/tests/base.py +++ b/src/openprocurement/api/tests/base.py @@ -89,6 +89,7 @@ def setUpClass(cls): def setUp(self): self.app.authorization = self.initial_auth self.db = self.app.recreate_db() + self.maxDiff = None def tearDown(self): self.app.drop_db() diff --git a/src/openprocurement/planning/api/constants.py b/src/openprocurement/planning/api/constants.py index a076c12c96..39d87c8789 100644 --- a/src/openprocurement/planning/api/constants.py +++ b/src/openprocurement/planning/api/constants.py @@ -13,6 +13,7 @@ "esco", "closeFrameworkAgreementUA", ), + "selective": ("priceQuotation",), "limited": ("reporting", "negotiation", "negotiation.quick"), } diff --git a/src/openprocurement/planning/api/tests/plan_blanks.py b/src/openprocurement/planning/api/tests/plan_blanks.py index 9a10bcdd87..1463fd2e95 100644 --- a/src/openprocurement/planning/api/tests/plan_blanks.py +++ b/src/openprocurement/planning/api/tests/plan_blanks.py @@ -908,7 +908,7 @@ def create_plan_invalid_procuring_entity(self): { u'description': u'procuringEntity with general kind cannot publish this type of procedure.' u' Procurement method types allowed for this kind: centralizedProcurement,' - u' reporting, negotiation, negotiation.quick, belowThreshold, aboveThresholdUA,' + u' reporting, negotiation, negotiation.quick, priceQuotation, belowThreshold, aboveThresholdUA,' u' aboveThresholdEU, competitiveDialogueUA, competitiveDialogueEU, esco, ' u'closeFrameworkAgreementUA.', u'location': u'procuringEntity', u'name': u'kind' } diff --git a/src/openprocurement/planning/api/tests/plan_status.py b/src/openprocurement/planning/api/tests/plan_status.py index c9a0f6efe0..fc67d3449a 100644 --- a/src/openprocurement/planning/api/tests/plan_status.py +++ b/src/openprocurement/planning/api/tests/plan_status.py @@ -395,7 +395,7 @@ def test_fail_complete_manually(app, value): assert response.json["errors"] == [ {u'description': u'procuringEntity with general kind cannot publish this type of procedure.' u' Procurement method types allowed for this kind: centralizedProcurement, reporting,' - u' negotiation, negotiation.quick, belowThreshold, aboveThresholdUA, aboveThresholdEU,' + u' negotiation, negotiation.quick, priceQuotation, belowThreshold, aboveThresholdUA, aboveThresholdEU,' u' competitiveDialogueUA, competitiveDialogueEU, esco, closeFrameworkAgreementUA.', u'location': u'procuringEntity', u'name': u'kind' } diff --git a/src/openprocurement/planning/api/tests/plan_tenders.py b/src/openprocurement/planning/api/tests/plan_tenders.py index af84bfb8a5..4fb4371ac7 100644 --- a/src/openprocurement/planning/api/tests/plan_tenders.py +++ b/src/openprocurement/planning/api/tests/plan_tenders.py @@ -19,6 +19,7 @@ from openprocurement.tender.openua.tests.base import test_tender_data as openua_tender_data from openprocurement.tender.openuadefense.tests.base import test_tender_data as defense_tender_data from openprocurement.tender.cfaselectionua.tests.tender import tender_data as cfa_selection_tender_data +from openprocurement.tender.pricequotation.tests.data import test_tender_data as pricequotation_tender_data from copy import deepcopy import pytest @@ -292,6 +293,7 @@ def test_fail_tender_creation(app): openeu_tender_data, openua_tender_data, defense_tender_data, + pricequotation_tender_data ] diff --git a/src/openprocurement/planning/api/tests/tests.ini b/src/openprocurement/planning/api/tests/tests.ini index 40e3459385..767354d7ad 100644 --- a/src/openprocurement/planning/api/tests/tests.ini +++ b/src/openprocurement/planning/api/tests/tests.ini @@ -26,6 +26,7 @@ plugins = tender.esco, tender.cfaua, tender.cfaselectionua, + tender.pricequotation, planning.api, contracting.api update_after = false diff --git a/src/openprocurement/tender/belowthreshold/views/bid.py b/src/openprocurement/tender/belowthreshold/views/bid.py index 2fdda1ae55..d523381f5c 100644 --- a/src/openprocurement/tender/belowthreshold/views/bid.py +++ b/src/openprocurement/tender/belowthreshold/views/bid.py @@ -24,7 +24,11 @@ class TenderBidResource(APIResource): @json_view( content_type="application/json", permission="create_bid", - validators=(validate_bid_data, validate_bid_operation_not_in_tendering, validate_bid_operation_period), + validators=( + validate_bid_operation_not_in_tendering, + validate_bid_data, + validate_bid_operation_period + ), ) def collection_post(self): """Registration of new bid proposal diff --git a/src/openprocurement/tender/core/constants.py b/src/openprocurement/tender/core/constants.py index 0fe9446d81..e3e6b819ca 100644 --- a/src/openprocurement/tender/core/constants.py +++ b/src/openprocurement/tender/core/constants.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta from openprocurement.api.constants import TZ, CPV_ITEMS_CLASS_FROM from openprocurement.tender.competitivedialogue.constants import CD_UA_TYPE, CD_EU_TYPE +from openprocurement.tender.pricequotation.constants import PMT as PRICEQUOTATION + BIDDER_TIME = timedelta(minutes=6) SERVICE_TIME = timedelta(minutes=9) @@ -27,6 +29,7 @@ "aboveThresholdEU", "aboveThresholdUA", "aboveThresholdUA.defense", + PRICEQUOTATION } diff --git a/src/openprocurement/tender/core/traversal.py b/src/openprocurement/tender/core/traversal.py index 7876049f31..80c0a06c1a 100644 --- a/src/openprocurement/tender/core/traversal.py +++ b/src/openprocurement/tender/core/traversal.py @@ -25,6 +25,7 @@ class Root(object): (Allow, "g:Administrator", "edit_complaint"), (Allow, "g:admins", ALL_PERMISSIONS), (Allow, "g:bots", "upload_tender_documents"), + (Allow, "g:bots", "edit_tender"), (Allow, "g:bots", "upload_qualification_documents"), ] diff --git a/src/openprocurement/tender/core/validation.py b/src/openprocurement/tender/core/validation.py index a16c05e843..9eda2ee98a 100644 --- a/src/openprocurement/tender/core/validation.py +++ b/src/openprocurement/tender/core/validation.py @@ -1515,7 +1515,8 @@ def validate_tender_matches_plan(request): pattern = plan.classification.id[:3] if plan.classification.id.startswith("336") else plan.classification.id[:4] for i, item in enumerate(tender.items): - if item.classification.id[: len(pattern)] != pattern: + # item.classification may be empty in pricequotaiton + if item.classification and item.classification.id[: len(pattern)] != pattern: request.errors.add( "data", "items[{}].classification.id".format(i), diff --git a/src/openprocurement/tender/pricequotation/__init__.py b/src/openprocurement/tender/pricequotation/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/__init__.py @@ -0,0 +1 @@ + diff --git a/src/openprocurement/tender/pricequotation/adapters.py b/src/openprocurement/tender/pricequotation/adapters.py new file mode 100644 index 0000000000..3e2b8faf92 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/adapters.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from openprocurement.tender.core.adapters import TenderConfigurator +from openprocurement.tender.pricequotation.models import PriceQuotationTender + + +class PQTenderConfigurator(TenderConfigurator): + """ Price Quotation Tender configuration adapter """ + + name = "Price Quotation Tender configurator" + model = PriceQuotationTender diff --git a/src/openprocurement/tender/pricequotation/constants.py b/src/openprocurement/tender/pricequotation/constants.py new file mode 100644 index 0000000000..d292ebb34e --- /dev/null +++ b/src/openprocurement/tender/pricequotation/constants.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +import re +from datetime import timedelta + + +PMT = "priceQuotation" +QUALIFICATION_DURATION = timedelta(days=2) +PROFILE_PATTERN = re.compile(r"^\d{6}-\d{8}-\d{6}-\d{8}") +PQ_KINDS = ["general", "special", "defense", "other", "social", "authority"] diff --git a/src/openprocurement/tender/pricequotation/includeme.py b/src/openprocurement/tender/pricequotation/includeme.py new file mode 100644 index 0000000000..ee5cee9ea5 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/includeme.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import os +import openprocurement.tender.pricequotation + +from logging import getLogger +from pyramid.interfaces import IRequest +from openprocurement.api.interfaces import IContentConfigurator +from openprocurement.tender.pricequotation.interfaces import\ + IPriceQuotationTender +from openprocurement.tender.pricequotation.models.tender import\ + PriceQuotationTender +from openprocurement.tender.pricequotation.adapters import\ + PQTenderConfigurator + + +LOGGER = getLogger("openprocurement.tender.pricequotation") + + +def includeme(config): + LOGGER.info("Init tender.pricequotation plugin.") + config.add_tender_procurementMethodType(PriceQuotationTender) + config.scan("openprocurement.tender.pricequotation.views") + config.scan("openprocurement.tender.pricequotation.subscribers") + config.registry.registerAdapter( + PQTenderConfigurator, + (IPriceQuotationTender, IRequest), + IContentConfigurator + ) diff --git a/src/openprocurement/tender/pricequotation/interfaces.py b/src/openprocurement/tender/pricequotation/interfaces.py new file mode 100644 index 0000000000..1f03bdf39b --- /dev/null +++ b/src/openprocurement/tender/pricequotation/interfaces.py @@ -0,0 +1,5 @@ +from openprocurement.tender.core.models import ITender + + +class IPriceQuotationTender(ITender): + """ PriceQuotation Tender marker interface """ diff --git a/src/openprocurement/tender/pricequotation/models/__init__.py b/src/openprocurement/tender/pricequotation/models/__init__.py new file mode 100644 index 0000000000..6340024740 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/__init__.py @@ -0,0 +1,5 @@ +from openprocurement.tender.pricequotation.models.cancellation import Cancellation +from openprocurement.tender.pricequotation.models.award import Award +from openprocurement.tender.pricequotation.models.document import Document +from openprocurement.tender.pricequotation.models.bid import Bid +from openprocurement.tender.pricequotation.models.tender import PriceQuotationTender diff --git a/src/openprocurement/tender/pricequotation/models/award.py b/src/openprocurement/tender/pricequotation/models/award.py new file mode 100644 index 0000000000..06e32789e8 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/award.py @@ -0,0 +1,47 @@ +from schematics.transforms import whitelist, blacklist +from schematics.types import MD5Type +from openprocurement.api.models import\ + schematics_default_role, schematics_embedded_role +from openprocurement.tender.core.models import BaseAward +from openprocurement.tender.pricequotation.utils import get_bid_owned_award_acl + + +class Award(BaseAward): + """ An award for the given procurement. There may be more than one award + per contracting process e.g. because the contract is split amongst + different providers, or because it is a standing offer. + """ + + class Options: + roles = { + "create": blacklist("id", "status", "date", "documents"), + "edit": whitelist( + "status", "title", "title_en", "title_ru", + "description", "description_en", "description_ru" + ), + "edit_tender_owner": whitelist( + "status", "title", "title_en", "title_ru", + "description", "description_en", "description_ru" + ), + "edit_bid_owner": whitelist( + "status", "title", "title_en", "title_ru", + "description", "description_en", "description_ru" + ), + "embedded": schematics_embedded_role, + "view": schematics_default_role, + "Administrator": whitelist(), + } + + bid_id = MD5Type(required=True) + + def __acl__(self): + return get_bid_owned_award_acl(self) + + def get_role(self): + root = self.get_root() + request = root.request + if request.authenticated_role in ("tender_owner", "bid_owner"): + role = "edit_{}".format(request.authenticated_role) + else: + role = request.authenticated_role + return role diff --git a/src/openprocurement/tender/pricequotation/models/bid.py b/src/openprocurement/tender/pricequotation/models/bid.py new file mode 100644 index 0000000000..5c85246025 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/bid.py @@ -0,0 +1,112 @@ +from uuid import uuid4 + +from pyramid.security import Allow +from schematics.types import MD5Type, StringType +from schematics.types.compound import ModelType +from schematics.transforms import whitelist + +from openprocurement.api.utils import get_now +from openprocurement.api.models import BusinessOrganization +from openprocurement.api.models import ( + ListType, Value, IsoDateTimeType + ) +from openprocurement.tender.core.models import Model +from openprocurement.tender.core.models import ( + Administrator_bid_role, + view_bid_role, +) +from openprocurement.tender.pricequotation.models.document import\ + Document +from openprocurement.tender.pricequotation.validation import\ + validate_bid_value, validate_requirement_responses + + +class RequirementReference(Model): + id = StringType(required=True) + title = StringType() + + +class RequirementResponse(Model): + id = MD5Type(required=True, default=lambda: uuid4().hex) + requirement = ModelType(RequirementReference, required=True) + value = StringType(required=True) + + +class Bid(Model): + class Options: + roles = { + "Administrator": Administrator_bid_role, + "embedded": view_bid_role, + "view": view_bid_role, + "create": whitelist( + "value", + "status", + "tenderers", + "parameters", + "documents", + "requirementResponses" + ), + "edit": whitelist("value", "status", "tenderers"), + "active.tendering": whitelist(), + "active.qualification": view_bid_role, + "active.awarded": view_bid_role, + "complete": view_bid_role, + "unsuccessful": view_bid_role, + "cancelled": view_bid_role, + } + + def __local_roles__(self): + return dict([("{}_{}".format(self.owner, self.owner_token), + "bid_owner")]) + + tenderers = ListType( + ModelType(BusinessOrganization, required=True), + required=True, + min_size=1, + max_size=1 + ) + date = IsoDateTimeType(default=get_now) + id = MD5Type(required=True, default=lambda: uuid4().hex) + status = StringType(choices=["active", "draft"], default="active") + value = ModelType(Value) + documents = ListType(ModelType(Document, required=True), default=list()) + owner_token = StringType() + transfer_token = StringType() + owner = StringType() + requirementResponses = ListType( + ModelType(RequirementResponse), + required=True, + min_size=1, + ) + + def import_data(self, raw_data, **kw): + """ + Converts and imports the raw data into the instance of the model + according to the fields in the model. + + :param raw_data: + The data to be imported. + """ + data = self.convert(raw_data, **kw) + del_keys = [k for k in data.keys() if k != "value" and data[k] is None] + for k in del_keys: + del data[k] + + self._data.update(data) + return self + + def __acl__(self): + return [ + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_bid") + ] + + def validate_value(self, data, value): + parent = data["__parent__"] + if isinstance(parent, Model): + validate_bid_value(parent, value) + + def validate_requirementResponses(self, data, value): + criterion = data["__parent__"]['criteria'] + validate_requirement_responses( + criterion, value + ) diff --git a/src/openprocurement/tender/pricequotation/models/cancellation.py b/src/openprocurement/tender/pricequotation/models/cancellation.py new file mode 100644 index 0000000000..b4616d0f86 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/cancellation.py @@ -0,0 +1,45 @@ +from uuid import uuid4 +from schematics.types import MD5Type, StringType +from schematics.transforms import whitelist +from schematics.types.compound import ModelType + +from openprocurement.api.utils import get_now +from openprocurement.api.models import IsoDateTimeType, ListType +from openprocurement.tender.core.models import Model +from openprocurement.api.models import\ + schematics_default_role, schematics_embedded_role +from openprocurement.tender.pricequotation.models.document import\ + Document + + +class Cancellation(Model): + class Options: + roles = { + "create": whitelist( + "reason", + "reasonType", + "cancellationOf", + ), + "edit": whitelist("status", "reasonType"), + "embedded": schematics_embedded_role, + "view": schematics_default_role, + } + + id = MD5Type(required=True, default=lambda: uuid4().hex) + reason = StringType(required=True) + reason_en = StringType() + reason_ru = StringType() + date = IsoDateTimeType(default=get_now) + status = StringType( + choices=["draft", "unsuccessful", "active"], + default='draft' + ) + documents = ListType(ModelType(Document, required=True), default=list()) + cancellationOf = StringType( + required=True, + choices=["tender"], + default="tender" + ) + reasonType = StringType( + choices=["noDemand", "unFixable", "forceMajeure", "expensesCut"], + ) diff --git a/src/openprocurement/tender/pricequotation/models/criterion.py b/src/openprocurement/tender/pricequotation/models/criterion.py new file mode 100644 index 0000000000..97d17132f3 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/criterion.py @@ -0,0 +1,21 @@ +from schematics.types import StringType +from schematics.types.compound import ModelType + +from openprocurement.api.models import ListType, Model +from openprocurement.tender.pricequotation.models.requirement import Requirement +from openprocurement.tender.pricequotation.validation import validate_requirement_groups + + +class RequirementGroup(Model): + id = StringType(required=True) + description = StringType(required=True) + requirements = ListType(ModelType(Requirement, required=True), default=list()) + + +class Criterion(Model): + id = StringType(required=True) + title = StringType(required=True) + description = StringType(required=True) + requirementGroups = ListType(ModelType(RequirementGroup), + required=True, + validators=[validate_requirement_groups]) diff --git a/src/openprocurement/tender/pricequotation/models/document.py b/src/openprocurement/tender/pricequotation/models/document.py new file mode 100644 index 0000000000..3c8a644dfe --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/document.py @@ -0,0 +1,21 @@ +from schematics.types import StringType +from schematics.exceptions import ValidationError +from openprocurement.tender.core.models import BaseDocument, Model, get_tender + + +class Document(BaseDocument): + documentOf = StringType( + required=True, + choices=["tender", "item"], + default="tender" + ) + + def validate_relatedItem(self, data, relatedItem): + if not relatedItem and data.get("documentOf") in ["item"]: + raise ValidationError(u"This field is required.") + parent = data["__parent__"] + if relatedItem and isinstance(parent, Model): + tender = get_tender(parent) + items = [i.id for i in tender.items if i] + if data.get("documentOf") == "item" and relatedItem not in items: + raise ValidationError(u"relatedItem should be one of items") diff --git a/src/openprocurement/tender/pricequotation/models/requirement.py b/src/openprocurement/tender/pricequotation/models/requirement.py new file mode 100644 index 0000000000..1a598e62c0 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/requirement.py @@ -0,0 +1,32 @@ +from schematics.types import BaseType, BooleanType, FloatType, IntType, StringType, BaseType +from schematics.types.compound import ModelType +from schematics.exceptions import ValidationError + +from openprocurement.api.models import DecimalType, IsoDateTimeType, ListType, Model +from openprocurement.api.models import Unit as BaseUnit +from openprocurement.tender.pricequotation.validation import validate_value_type + + +class Unit(BaseUnit): + name = StringType(required=True) + + +class Requirement(Model): + id = StringType(required=True) + title = StringType(required=True) + description = StringType() + dataType = StringType(required=True, + choices=["string", "number", "integer", "boolean"]) + unit = ModelType(Unit) + minValue = StringType() + maxValue = StringType() + expectedValue = StringType() + + def validate_minValue(self, data, value): + validate_value_type(value, data['dataType']) + + def validate_maxValue(self, data, value): + validate_value_type(value, data['dataType']) + + def validate_expectedValue(self, data, value): + validate_value_type(value, data['dataType']) diff --git a/src/openprocurement/tender/pricequotation/models/tender.py b/src/openprocurement/tender/pricequotation/models/tender.py new file mode 100644 index 0000000000..719f1b73d9 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/models/tender.py @@ -0,0 +1,356 @@ +# -*- coding: utf-8 -*- +from datetime import timedelta +from schematics.exceptions import ValidationError +from schematics.transforms import whitelist +from schematics.types import IntType, StringType +from schematics.types.compound import ModelType +from schematics.types.serializable import serializable +from pyramid.security import Allow +from zope.interface import implementer +from openprocurement.api.constants import TZ, CPV_ITEMS_CLASS_FROM +from openprocurement.api.models import\ + BusinessOrganization, CPVClassification, Guarantee, IsoDateTimeType +from openprocurement.api.models import Item as BaseItem +from openprocurement.api.models import\ + ListType, Period, Value +from openprocurement.api.utils import get_now +from openprocurement.api.validation import\ + validate_classification_id, validate_cpv_group, validate_items_uniq +from openprocurement.tender.core.utils import calculate_tender_business_date +from openprocurement.tender.core.models import ( + Contract as BaseContract, + PeriodEndRequired, + ProcuringEntity, + Tender, + Model + ) +from openprocurement.tender.pricequotation.constants import PMT,\ + QUALIFICATION_DURATION, PQ_KINDS, PROFILE_PATTERN +from openprocurement.tender.pricequotation.interfaces\ + import IPriceQuotationTender + +from openprocurement.tender.pricequotation.models import ( + Cancellation, + Bid, + Document, + Award + ) +from openprocurement.tender.pricequotation.models.criterion import Criterion + + +class ShortlistedFirm(BusinessOrganization): + id = StringType() + status = StringType() + + +class Item(BaseItem): + class Options: + roles = { + 'create': whitelist( + 'id', + 'description', + 'description_en', + 'description_ru', + 'quantity', + 'deliveryDate', + 'deliveryAddress', + 'deliveryLocation' + ), + 'edit': whitelist( + 'description', + 'description_en', + 'description_ru', + 'quantity', + 'deliveryDate', + 'deliveryAddress', + 'deliveryLocation', + ), + 'bots': whitelist( + 'classification', + 'additionalClassifications', + 'unit' + ), + "edit_contract": whitelist("unit") + } + + """A good, service, or work to be contracted.""" + classification = ModelType(CPVClassification) + + +class Contract(BaseContract): + documents = ListType(ModelType(Document, required=True), default=list()) + + def validate_dateSigned(self, data, value): + parent = data["__parent__"] + if value and isinstance(parent, Model): + if value > get_now(): + raise ValidationError(u"Contract signature date can't be in the future") + + + +@implementer(IPriceQuotationTender) +class PriceQuotationTender(Tender): + """ + Data regarding tender process - publicly inviting prospective contractors + to submit bids for evaluation and selecting a winner or winners. + """ + + class Options: + namespace = "Tender" + _core_roles = Tender.Options.roles + # without _serializable_fields they won't be calculated + # (even though serialized_name is in the role) + _serializable_fields = whitelist( + "tender_guarantee", + "tender_value", + "tender_minimalStep" + ) + _edit_fields = _serializable_fields + whitelist( + "next_check", + "numberOfBidders", + "items", + "tenderPeriod", + "procuringEntity", + "guarantee", + "minimalStep", + ) + _edit_role = _core_roles["edit"] \ + + _edit_fields + whitelist( + "contracts", + "numberOfBids", + "status", + "value", + "profile" + ) + _create_role = _core_roles["create"] \ + + _core_roles["edit"] \ + + _edit_fields \ + + whitelist("contracts", + "numberOfBids", + "value", + "profile") + _edit_pq_bot_role = whitelist( + "items", "shortlistedFirms", + "status", "criteria", "value", "unsuccessfulReason" + ) + _view_tendering_role = ( + _core_roles["view"] + + _edit_fields + + whitelist( + "awards", + 'value', + "awardPeriod", + "cancellations", + "contracts", + "profile", + "shortlistedFirms", + "criteria", + "noticePublicationDate", + "unsuccessfulReason" + ) + ) + _view_role = _view_tendering_role + whitelist("bids", "numberOfBids") + _all_forbidden = whitelist() + roles = { + "create": _create_role, + "edit": _edit_role, + "edit_draft": _edit_role, + "edit_draft.unsuccessful": _edit_role, + "edit_draft.publishing": _edit_pq_bot_role, + "edit_active.tendering": _all_forbidden, + "edit_active.qualification": _all_forbidden, + "edit_active.awarded": _all_forbidden, + "edit_complete": _all_forbidden, + "edit_unsuccessful": _all_forbidden, + "edit_cancelled": _all_forbidden, + "draft": _view_tendering_role, + "draft.unsuccessful": _view_tendering_role, + "draft.publishing": _view_tendering_role, + "active.tendering": _view_tendering_role, + "view": _view_role, + "active.qualification": _view_role, + "active.awarded": _view_role, + "complete": _view_role, + "unsuccessful": _view_role, + "cancelled": _view_role, + "chronograph": _core_roles["chronograph"], + "chronograph_view": _core_roles["chronograph_view"], + "Administrator": _core_roles["Administrator"], + "plain": _core_roles["plain"], + "listing": _core_roles["listing"], + "contracting": _core_roles["contracting"], + "default": _core_roles["default"], + "bots": _edit_pq_bot_role, + } + + status = StringType(choices=["draft", + "draft.publishing", + "draft.unsuccessful", + "active.tendering", + "active.qualification", + "active.awarded", + "complete", + "cancelled", + "unsuccessful"], + default="draft") + + # The goods and services to be purchased, + # broken into line items wherever possible. + # Items should not be duplicated, but a quantity of 2 specified instead. + items = ListType( + ModelType(Item, required=True), + required=True, + min_size=1, + validators=[validate_items_uniq], + ) + # The total estimated value of the procurement. + value = ModelType(Value, required=True) + # The period when the tender is open for submissions. + # The end date is the closing date for tender submissions. + tenderPeriod = ModelType( + PeriodEndRequired, required=True + ) + # The date or period on which an award is anticipated to be made. + awardPeriod = ModelType(Period) + # The number of unique tenderers who participated in the tender + numberOfBidders = IntType() + # A list of all the companies who entered submissions for the tender. + bids = ListType( + ModelType(Bid, required=True), default=list() + ) + # The entity managing the procurement, + # which may be different from the buyer + # who is paying / using the items being procured. + procuringEntity = ModelType(ProcuringEntity, required=True) + awards = ListType(ModelType(Award, required=True), default=list()) + contracts = ListType(ModelType(Contract, required=True), default=list()) + cancellations = ListType( + ModelType(Cancellation, required=True), + default=list() + ) + documents = ListType( + ModelType(Document, required=True), default=list() + ) # All documents and attachments related to the tender. + guarantee = ModelType(Guarantee) + procurementMethod = StringType( + choices=["selective"], default="selective" + ) + procurementMethodType = StringType(default=PMT) + profile = StringType(required=True) + shortlistedFirms = ListType(ModelType(ShortlistedFirm), default=list()) + criteria = ListType(ModelType(Criterion), default=list()) + noticePublicationDate = IsoDateTimeType() + unsuccessfulReason = ListType(StringType) + + procuring_entity_kinds = PQ_KINDS + + def validate_buyers(self, data, value): + pass + + def validate_milestones(self, data, value): + # a hack to avoid duplicating all bese model fields + if value: + raise ValidationError("Milestones are not applicable to pricequotation") + + def get_role(self): + root = self.__parent__ + request = root.request + if request.authenticated_role in\ + ("Administrator", "chronograph", "contracting", "bots"): + role = request.authenticated_role + else: + role = "edit_{}".format(request.context.status) + return role + + @serializable(serialize_when_none=False) + def next_check(self): + checks = [] + if self.status == "active.tendering" and self.tenderPeriod.endDate: + checks.append(self.tenderPeriod.endDate.astimezone(TZ)) + + if self.status.startswith("active"): + for award in self.awards: + if award.status == 'pending': + checks.append( + calculate_tender_business_date(award.date, + QUALIFICATION_DURATION, + self) + ) + if award.status == "active" and not\ + any([i.awardID == award.id for i in self.contracts]): + checks.append(award.date) + return min(checks).isoformat() if checks else None + + @serializable + def numberOfBids(self): + """A property that is serialized by schematics exports.""" + return len(self.bids) + + def validate_items(self, data, items): + if data["status"] in ("draft", "draft.publishing", "draft.unsuccessful"): + return + if not all((i.classification for i in items)): + return + cpv_336_group = items[0].classification.id[:3] == "336"\ + if items else False + if ( + not cpv_336_group + and items + and len(set([i.classification.id[:4] for i in items])) != 1 + ): + raise ValidationError(u"CPV class of items should be identical") + else: + validate_cpv_group(items) + validate_classification_id(items) + + def validate_awardPeriod(self, data, period): + if ( + period + and period.startDate + and data.get("tenderPeriod") + and data.get("tenderPeriod").endDate + and period.startDate < data.get("tenderPeriod").endDate + ): + raise ValidationError(u"period should begin after tenderPeriod") + + def validate_tenderPeriod(self, data, period): + if ( + period + and period.startDate + and period.endDate + and period.endDate < calculate_tender_business_date(period.startDate, timedelta(days=2), data, True) + ): + raise ValidationError(u"the tenderPeriod cannot end earlier than 2 business days after the start") + + + def validate_profile(self, data, profile): + result = PROFILE_PATTERN.findall(profile) + if len(result) != 1: + raise ValidationError(u"The profile value doesn't match id pattern") + + def __local_roles__(self): + roles = dict([("{}_{}".format(self.owner, self.owner_token), "tender_owner")]) + for i in self.bids: + roles["{}_{}".format(i.owner, i.owner_token)] = "bid_owner" + return roles + + def _acl_contract(self, acl): + acl.extend([ + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), + ]) + + def _acl_cancellation(self, acl): + acl.extend([ + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_cancellation"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_tender"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_tender_documents"), + ]) + + def __acl__(self): + acl = [ + (Allow, "g:bots", "upload_award_documents"), + ] + self._acl_cancellation(acl) + self._acl_contract(acl) + return acl diff --git a/src/openprocurement/tender/pricequotation/subscribers.py b/src/openprocurement/tender/pricequotation/subscribers.py new file mode 100644 index 0000000000..80aeceb85c --- /dev/null +++ b/src/openprocurement/tender/pricequotation/subscribers.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from pyramid.events import subscriber +from openprocurement.tender.core.events import TenderInitializeEvent +from openprocurement.api.utils import get_now +from openprocurement.tender.pricequotation.constants import\ + PMT + + +@subscriber(TenderInitializeEvent, procurementMethodType=PMT) +def tender_init_handler(event): + """ Initialization handler for Price Quotation tenders """ + tender = event.tender + now = get_now() + + if not tender.tenderPeriod.startDate: + tender.tenderPeriod.startDate = now + tender.date = now diff --git a/src/openprocurement/tender/pricequotation/tests/__init__.py b/src/openprocurement/tender/pricequotation/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openprocurement/tender/pricequotation/tests/award.py b/src/openprocurement/tender/pricequotation/tests/award.py new file mode 100644 index 0000000000..0354e9d0a0 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/award.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +import unittest +from copy import deepcopy + +from openprocurement.api.tests.base import snitch +from openprocurement.tender.pricequotation.adapters import\ + PQTenderConfigurator as TenderBelowThersholdConfigurator +from openprocurement.tender.pricequotation.tests.base import ( + TenderContentWebTest, + test_bids, + test_organization, +) +from openprocurement.tender.pricequotation.tests.award_blanks import ( + check_tender_award, + create_tender_award_invalid, + check_tender_award_disqualification, + create_tender_award, + patch_tender_award, + tender_award_transitions, + check_tender_award_cancellation +) +from openprocurement.tender.belowthreshold.tests.award import ( + TenderAwardDocumentResourceTestMixin, + TenderAwardResourceTestMixin +) +from openprocurement.tender.belowthreshold.tests.award_blanks import ( + get_tender_award, + create_tender_award_with_scale_not_required, + create_tender_award_no_scale, + create_tender_award_no_scale_invalid, + patch_tender_award_Administrator_change, + create_tender_award_no_scale_invalid, +) + +class TenderAwardResourceTestMixin(object): + test_create_tender_award_invalid = snitch(create_tender_award_invalid) + test_create_tender_award_no_scale_invalid = snitch(create_tender_award_no_scale_invalid) + test_get_tender_award = snitch(get_tender_award) + + +class TenderAwardResourceTest(TenderContentWebTest, TenderAwardResourceTestMixin): + initial_status = "active.qualification" + initial_bids = test_bids + reverse = False + maxAwards = 1 + # init_awards = False + + test_create_tender_award = snitch(create_tender_award) + test_patch_tender_award = snitch(patch_tender_award) + test_tender_award_transitions = snitch(tender_award_transitions) + test_check_tender_award = snitch(check_tender_award) + test_check_tender_award_disqualification = snitch(check_tender_award_disqualification) + test_check_tender_award_cancellation = snitch(check_tender_award_cancellation) + + +class TenderAwardResourceScaleTest(TenderContentWebTest): + initial_status = "active.qualification" + initial_bids = test_bids + reverse = False + + test_create_tender_award_no_scale = snitch(create_tender_award_no_scale) + test_create_tender_award_no_scale_invalid = snitch( + create_tender_award_no_scale_invalid + ) + test_create_tender_award_with_scale_not_required = snitch( + create_tender_award_with_scale_not_required + ) + + +class TenderAwardDocumentResourceTest(TenderContentWebTest, TenderAwardDocumentResourceTestMixin): + initial_status = "active.qualification" + initial_bids = test_bids + + def setUp(self): + super(TenderAwardDocumentResourceTest, self).setUp() + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + self.awards_ids = [award["id"] for award in response.json["data"]] + + @property + def award_id(self): + data = self.db.get(self.tender_id) + return data['awards'][-1]['id'] if data.get('awards') else None + + +class TenderAwardDocumentWithDSResourceTest(TenderAwardDocumentResourceTest): + docservice = True + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderAwardDocumentResourceTest)) + suite.addTest(unittest.makeSuite(TenderAwardDocumentWithDSResourceTest)) + suite.addTest(unittest.makeSuite(TenderAwardResourceTest)) + suite.addTest(unittest.makeSuite(TenderAwardResourceScaleTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/award_blanks.py b/src/openprocurement/tender/pricequotation/tests/award_blanks.py new file mode 100644 index 0000000000..7b344538ce --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/award_blanks.py @@ -0,0 +1,596 @@ +# -*- coding: utf-8 -*- +from datetime import timedelta +from webtest import AppError +import mock + +from openprocurement.api.utils import get_now +from openprocurement.tender.core.tests.base import change_auth +from openprocurement.tender.pricequotation.tests.base import test_organization + + +def create_tender_award_invalid(self): + self.app.authorization = ("Basic", ("token", "")) + request_path = "/tenders/{}/awards?acc_token={}".format(self.tender_id, self.tender_token) + response = self.app.post(request_path, "data", status=415) + self.assertEqual(response.status, "415 Unsupported Media Type") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": u"Content-Type header should be one of ['application/json']", + u"location": u"header", + u"name": u"Content-Type", + } + ], + ) + + response = self.app.post(request_path, "data", content_type="application/json", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": u"No JSON object could be decoded", u"location": u"body", u"name": u"data"}], + ) + + response = self.app.post_json(request_path, "data", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"not_data": {}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"data": {"invalid_field": "invalid_value"}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Rogue field", u"location": u"body", u"name": u"invalid_field"}] + ) + + response = self.app.post_json(request_path, {"data": {"suppliers": [{"identifier": "invalid_value"}]}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": { + u"identifier": [u"Please use a mapping for this field or Identifier instance instead of unicode."] + }, + u"location": u"body", + u"name": u"suppliers", + } + ], + ) + + response = self.app.post_json(request_path, {"data": {"suppliers": [{"identifier": {"id": 0}}]}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [ + { + u"contactPoint": [u"This field is required."], + u"identifier": {u"scheme": [u"This field is required."]}, + u"name": [u"This field is required."], + u"address": [u"This field is required."], + } + ], + u"location": u"body", + u"name": u"suppliers", + }, + {u"description": [u"This field is required."], u"location": u"body", u"name": u"bid_id"}, + ], + ) + + response = self.app.post_json( + request_path, {"data": {"suppliers": [{"name": "name", "identifier": {"uri": "invalid_value"}}]}}, status=422 + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [ + { + u"contactPoint": [u"This field is required."], + u"identifier": { + u"scheme": [u"This field is required."], + u"id": [u"This field is required."], + u"uri": [u"Not a well formed URL."], + }, + u"address": [u"This field is required."], + } + ], + u"location": u"body", + u"name": u"suppliers", + }, + {u"description": [u"This field is required."], u"location": u"body", u"name": u"bid_id"}, + ], + ) + + response = self.app.post_json( + "/tenders/some_id/awards", + {"data": {"suppliers": [test_organization], "bid_id": self.initial_bids[0]["id"]}}, + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + response = self.app.get("/tenders/some_id/awards", status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + self.set_status("complete") + + response = self.app.post_json( + "/tenders/{}/awards".format(self.tender_id), + {"data": {"suppliers": [test_organization], "status": "pending", "bid_id": self.initial_bids[0]["id"]}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't create award in current (complete) tender status" + ) + + + +def create_tender_award(self): + with change_auth(self.app, ("Basic", ("token", ""))): + request_path = "/tenders/{}/awards".format(self.tender_id) + response = self.app.post_json( + request_path, + {"data": {"suppliers": [test_organization], "status": "pending", "bid_id": self.initial_bids[0]["id"]}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + award = response.json["data"] + self.assertEqual(award["suppliers"][0]["name"], test_organization["name"]) + self.assertIn("id", award) + self.assertIn(award["id"], response.headers["Location"]) + + response = self.app.get(request_path) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"][-1], award) + + award_request_path = "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award["id"], self.tender_token) + + response = self.app.patch_json(award_request_path, {"data": {"status": "active"}}, status=403) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json['status'], "error") + self.assertEqual( + response.json['errors'], + [{ + u'description': u"Can't change award status to active from pending", + u'location': u'body', + u'name': u'data' + }] + ) + + response = self.app.patch_json(award_request_path, {"data": {"status": "unsuccessful"}}) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "unsuccessful") + + response = self.app.get("/tenders/{}".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], u"unsuccessful") + + +def patch_tender_award(self): + request_path = "/tenders/{}/awards".format(self.tender_id) + response = self.app.patch_json( + "/tenders/{}/awards/some_id?acc_token={}".format(self.tender_id, self.tender_token), + {"data": {"status": "unsuccessful"}}, + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"award_id"}] + ) + + response = self.app.patch_json( + "/tenders/some_id/awards/some_id?acc_token={}".format(self.tender_token), + {"data": {"status": "unsuccessful"}}, + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + award_id = self.award_ids[0] + token = self.initial_bids_tokens[0] + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), + {"data": {"awardStatus": "unsuccessful"}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"], [{"location": "body", "name": "awardStatus", "description": "Rogue field"}] + ) + + token = self.initial_bids_tokens[0] + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), + {"data": {"status": "unsuccessful"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), + {"data": {"status": "pending"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Forbidden") + + response = self.app.get(request_path) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(len(response.json["data"]), 2) + new_award = response.json["data"][-1] + + token = self.initial_bids_tokens[1] + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], token), + {"data": {"title": "title", "description": "description"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["title"], "title") + self.assertEqual(response.json["data"]["description"], "description") + + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], token), + {"data": {"status": "active"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + + response = self.app.get(request_path) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(len(response.json["data"]), 2) + + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], self.tender_token), + {"data": {"status": "cancelled"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertIn("Location", response.headers) + + response = self.app.get(request_path) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(len(response.json["data"]), 3) + + self.set_status("complete") + + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["value"]["amount"], 469.0) + + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.tender_token), + {"data": {"status": "unsuccessful"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update award in current (complete) tender status" + ) + + +def tender_award_transitions(self): + award_id = self.award_ids[0] + tender_token = self.db.get(self.tender_id)['owner_token'] + bid_token = self.initial_bids_tokens[0] + # pending -> cancelled + for token_ in (tender_token, bid_token): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token_), + {"data": {"status": "cancelled"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + + # first award: tender_owner: forbidden + for status in ('active', 'unsuccessful'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": status}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"], [{ + "location": "url", + "name": "permission", + "description": "Forbidden" + }] + ) + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": 'unsuccessful'}}, + ) + self.assertEqual(response.status, "200 OK") + # bidOwner: unsuccessful -> ('active', 'cancelled', 'pending') must be forbidden + for status in ('active', 'cancelled', 'pending'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"], [{ + "location": "url", + "name": "permission", + "description": "Forbidden" + }] + ) + # tenderOwner: unsuccessful -> ('active', 'cancelled', 'pending') must be forbidden + for status in ('active', 'cancelled', 'pending'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"], [{ + "location": "body", + "name": "data", + "description": "Can't update award in current (unsuccessful) status" + }] + ) + tender = self.app.get("/tenders/{}".format(self.tender_id)).json['data'] + + award_id = tender['awards'][-1]['id'] + bid_token = self.initial_bids_tokens[1] + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": 'active'}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": 'active'}}, + ) + self.assertEqual(response.status, "200 OK") + for status in ('unsuccessful', 'cancelled', 'pending'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + for status in ('unsuccessful', 'pending'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": 'cancelled'}}, + ) + self.assertEqual(response.status, "200 OK") + tender = self.app.get("/tenders/{}".format(self.tender_id)).json['data'] + award_id = tender['awards'][-1]['id'] + for status in ('unsuccessful', 'cancelled', 'active'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": 'cancelled'}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": 'unsuccessful'}}, + ) + self.assertEqual(response.status, "200 OK") + + +def check_tender_award(self): + # get bids + response = self.app.get("/tenders/{}/bids".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + bids = response.json["data"] + sorted_bids = sorted(bids, key=lambda bid: bid["value"]['amount']) + + # get awards + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + # get pending award + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # check award + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["suppliers"][0]["name"], sorted_bids[0]["tenderers"][0]["name"]) + self.assertEqual( + response.json["data"]["suppliers"][0]["identifier"]["id"], sorted_bids[0]["tenderers"][0]["identifier"]["id"] + ) + self.assertEqual(response.json["data"]["bid_id"], sorted_bids[0]["id"]) + + # cancel award + token = self.initial_bids_tokens[0] + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), + {"data": {"status": "unsuccessful"}}, + ) + self.assertEqual(response.status, "200 OK") + + # get awards + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + # get pending award + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # check new award + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["suppliers"][0]["name"], sorted_bids[1]["tenderers"][0]["name"]) + self.assertEqual( + response.json["data"]["suppliers"][0]["identifier"]["id"], sorted_bids[1]["tenderers"][0]["identifier"]["id"] + ) + self.assertEqual(response.json["data"]["bid_id"], sorted_bids[1]["id"]) + + + +def check_tender_award_disqualification(self): + # get bids + response = self.app.get("/tenders/{}/bids".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + bids = response.json["data"] + sorted_bids = sorted(bids, key=lambda bid: bid["value"]['amount']) + + # get awards + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + # get pending award + award = [i for i in response.json["data"] if i["status"] == "pending"][0] + award_id = award['id'] + # check award + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["suppliers"][0]["name"], sorted_bids[0]["tenderers"][0]["name"]) + self.assertEqual( + response.json["data"]["suppliers"][0]["identifier"]["id"], sorted_bids[0]["tenderers"][0]["identifier"]["id"] + ) + self.assertEqual(response.json["data"]["bid_id"], sorted_bids[0]["id"]) + + # wait 2 days + date = (get_now() - timedelta(days=2)).isoformat() + self.tender_document_patch = self.db.get(self.tender_id) + self.tender_document_patch['awards'][0]['date'] = date + self.save_changes() + self.check_chronograph() + + # get awards + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + # # get pending award + awards = response.json['data'] + self.assertEqual(len(awards), 2) + self.assertEqual(awards[0]['status'], "unsuccessful") + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # check new award + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["suppliers"][0]["name"], sorted_bids[1]["tenderers"][0]["name"]) + self.assertEqual( + response.json["data"]["suppliers"][0]["identifier"]["id"], sorted_bids[1]["tenderers"][0]["identifier"]["id"] + ) + self.assertEqual(response.json["data"]["bid_id"], sorted_bids[1]["id"]) + + +def check_tender_award_cancellation(self): + # get bids + response = self.app.get("/tenders/{}/bids".format(self.tender_id)) + bids = response.json["data"] + bid_token = self.initial_bids_tokens[0] + tender_token = self.db.get(self.tender_id)['owner_token'] + sorted_bids = sorted(bids, key=lambda bid: bid["value"]['amount']) + + # get awards + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + # get pending award + award = [i for i in response.json["data"] if i["status"] == "pending"][0] + award_id = award['id'] + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": "active"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json['data']['status'], "active") + + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": "cancelled"}}, + ) + + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json['data']['status'], "cancelled") + old_award = response.json['data'] + + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + + award = [i for i in response.json["data"] if i["status"] == "pending"][-1] + award_id = award['id'] + self.assertEqual(old_award['bid_id'], award['bid_id']) + + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token), + {"data": {"status": "active"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json['status'], "error") + + for status in ('active', 'cancelled'): + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token), + {"data": {"status": status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json['status'], "error") + self.assertEqual(response.json['errors'], [{ + u'description': u"Can't change award status to {} from pending".format(status), + u'location': u'body', + u'name': u'data' + }]) diff --git a/src/openprocurement/tender/pricequotation/tests/base.py b/src/openprocurement/tender/pricequotation/tests/base.py new file mode 100644 index 0000000000..71b27bfd89 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/base.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +import os +from copy import deepcopy +from uuid import uuid4 + +from datetime import timedelta + +from openprocurement.api.constants import SANDBOX_MODE, RELEASE_2020_04_19 +from openprocurement.api.tests.base import BaseWebTest +from openprocurement.tender.core.tests.base import BaseCoreWebTest +from openprocurement.api.constants import TZ +from openprocurement.tender.belowthreshold.constants import MIN_BIDS_NUMBER +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.pricequotation.tests.data import * + + +class BaseApiWebTest(BaseWebTest): + relative_to = os.path.dirname(__file__) + + +class BaseTenderWebTest(BaseCoreWebTest): + relative_to = os.path.dirname(__file__) + initial_data = test_tender_data + initial_status = None + maxDiff = None + + initial_bids = None + initial_auth = ("Basic", ("broker", "")) + docservice = False + min_bids_number = MIN_BIDS_NUMBER + # Statuses for test, that will be imported from others procedures + primary_tender_status = "draft.publishing" # status, to which tender should be switched from 'draft' + forbidden_document_modification_actions_status = ( + "active.qualification" + ) # status, in which operations with tender documents (adding, updating) are forbidden + forbidden_question_modification_actions_status = ( + "active.tendering" + ) # status, in which adding/updating tender questions is forbidden + forbidden_contract_document_modification_actions_status = ( + "unsuccessful" + ) # status, in which operations with tender's contract documents (adding, updating) are forbidden + forbidden_auction_document_create_actions_status = ( + "active.tendering" + ) # status, in which adding document to tender auction is forbidden + maxAwards = 2 + periods = PERIODS + meta_initial_bids = test_bids + init_awards = True + + def generate_awards(self, status, startend): + bids = self.tender_document.get("bids", []) or self.tender_document_patch.get("bids", []) + awardPeriod_startDate = (self.now + self.periods[status][startend]["awardPeriod"]["startDate"]).isoformat() + if "awards" not in self.tender_document and self.init_awards: + self.award_ids = [] + self.tender_document_patch["awards"] = [] + for bid in bids: + id_ = uuid4().hex + award = { + "status": "pending", + "suppliers": bid["tenderers"], + "bid_id": bid["id"], + "value": bid["value"], + 'items': self.tender_document['items'], + "date": awardPeriod_startDate, + "documents": [], + "id": id_, + } + self.tender_document_patch["awards"].append(award) + self.award_ids.append(id_) + if len(self.tender_document_patch["awards"]) == self.maxAwards: + break + self.save_changes() + + def activate_awards(self): + awards = self.tender_document.get("awards", []) + if awards: + for award in awards: + if award["status"] == "pending": + award.update({"status": "active"}) + self.tender_document_patch.update({"awards": awards}) + self.save_changes() + + def generate_bids(self, status, startend): + tenderPeriod_startDate = self.now + self.periods[status][startend]["tenderPeriod"]["startDate"] + bids = self.tender_document.get("bids", []) + if self.initial_bids and not bids: + self.tender_document_patch["bids"] = [] + self.initial_bids_tokens = [] + for position, bid in enumerate(test_bids): + bid = deepcopy(bid) + token = uuid4().hex + bid.update( + { + "id": uuid4().hex, + "date": (tenderPeriod_startDate + timedelta(seconds=(position + 1))).isoformat(), + "owner_token": token, + "status": "draft", + "owner": "broker", + } + ) + self.tender_document_patch["bids"].append(bid) + self.initial_bids_tokens.append(token) + self.save_changes() + response = self.app.get('/tenders/{}/bids'.format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.initial_bids = response.json["data"] + + def generate_contract(self): + awards = self.tender_document.get("awards", []) + contracts = self.tender_document.get("contracts", []) + + if not contracts: + for award in reversed(awards): + if award["status"] == "active": + if award["value"]["valueAddedTaxIncluded"]: + amount_net = float(award["value"]["amount"]) - 1 + else: + amount_net = award["value"]["amount"] + contract = { + "id": uuid4().hex, + "title": "contract title", + "description": "contract description", + "awardID": award["id"], + "value": { + "amount": award["value"]["amount"], + "amountNet": amount_net, + "currency": award["value"]["currency"], + "valueAddedTaxIncluded": award["value"]["valueAddedTaxIncluded"], + }, + "suppliers": award["suppliers"], + "status": "pending", + "contractID": "UA-2017-06-21-000001-1", + "date": datetime.now(TZ).isoformat(), + "items": self.tender_document["items"], + } + self.contract_id = contract["id"] + self.tender_document_patch.update({"contracts": [contract]}) + self.save_changes() + + def set_status(self, status, startend="start", extra=None): + self.now = get_now() + self.tender_document = self.db.get(self.tender_id) + self.tender_document_patch = {"status": status} + self.patch_tender_bot() + if status == "active.tendering": + self.update_periods(status, startend) + elif status == "active.qualification": + self.update_periods(status, startend) + self.generate_bids(status, startend) + self.generate_awards(status, startend) + elif status == "active.awarded": + self.update_periods(status, startend) + self.generate_bids(status, startend) + self.generate_awards(status, startend) + self.activate_awards() + self.generate_contract() + elif status == "complete": + self.update_periods(status, startend) + self.generate_bids(status, startend) + self.generate_awards(status, startend) + self.activate_awards() + self.generate_contract() + return self.get_tender("chronograph") + + def update_periods(self, status, startend): + for period in self.periods[status][startend]: + self.tender_document_patch.update({period: {}}) + for date in self.periods[status][startend][period]: + self.tender_document_patch[period][date] = ( + self.now + self.periods[status][startend][period][date] + ).isoformat() + self.save_changes() + + def patch_tender_bot(self): + items = deepcopy(self.initial_data["items"]) + for item in items: + item.update({ + "classification": test_short_profile["classification"], + "unit": test_short_profile["unit"] + }) + value = deepcopy(test_short_profile['value']) + amount = sum([item["quantity"] for item in items]) * test_short_profile['value']['amount'] + value["amount"] = amount + criteria = getattr(self, "test_criteria", test_short_profile['criteria']) + self.tender_document_patch.update({ + "shortlistedFirms": test_shortlisted_firms, + 'criteria': criteria, + "items": items, + 'value': value + }) + self.save_changes() + + @property + def tender_token(self): + data = self.db.get(self.tender_id) + award = data['awards'][-1] if data.get('awards') else None + if award and award['status'] == 'pending': + bid = [b for b in data['bids'] if b['id'] == award['bid_id']][0] + return bid['owner_token'] + else: + return data['owner_token'] + + def create_tender(self): + data = deepcopy(self.initial_data) + response = self.app.post_json("/tenders", {"data": data}) + tender = response.json["data"] + self.tender_id = tender["id"] + status = tender["status"] + if self.initial_status and self.initial_status != status: + self.set_status(self.initial_status) + + +class TenderContentWebTest(BaseTenderWebTest): + initial_data = test_tender_data + initial_status = None + initial_bids = None + need_tender = True + def setUp(self): + super(TenderContentWebTest, self).setUp() + if self.need_tender: + self.create_tender() diff --git a/src/openprocurement/tender/pricequotation/tests/bid.py b/src/openprocurement/tender/pricequotation/tests/bid.py new file mode 100644 index 0000000000..7e11c12f33 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/bid.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +import unittest + +from openprocurement.api.tests.base import snitch +from openprocurement.tender.pricequotation.tests.base import ( + TenderContentWebTest, + test_organization, + test_bids, + test_requirement_response_valid, +) +from openprocurement.tender.pricequotation.tests.data import ( + test_criteria_1, + test_criteria_2, + test_criteria_3, + test_criteria_4 + ) +from openprocurement.tender.belowthreshold.tests.bid_blanks import ( + create_tender_bid_with_document_invalid, + create_tender_bid_with_document, + create_tender_bid_with_documents, + create_tender_bid_with_document_invalid, + create_tender_bid_with_document, + create_tender_bid_with_documents, + create_tender_bid_document_json, + put_tender_bid_document_json, + not_found, + create_tender_bid_document, + put_tender_bid_document, + ) +from openprocurement.tender.pricequotation.tests.bid_blanks import ( + create_tender_bid, + create_tender_bid_document_nopending, + create_tender_bid_invalid, + patch_tender_bid, + get_tender_bid, + delete_tender_bid, + get_tender_tenderers, + bid_Administrator_change, + patch_tender_bid_document, + requirement_response_validation_multiple_criterias, + requirement_response_validation_multiple_groups, + requirement_response_validation_multiple_groups_multiple_requirements, + requirement_response_validation_one_group_multiple_requirements +) + + +class TenderBidResourceTest(TenderContentWebTest): + initial_status = "active.tendering" + + test_create_tender_bid_invalid = snitch(create_tender_bid_invalid) + test_create_tender_bid = snitch(create_tender_bid) + test_patch_tender_bid = snitch(patch_tender_bid) + test_get_tender_bid = snitch(get_tender_bid) + test_delete_tender_bid = snitch(delete_tender_bid) + test_get_tender_tenderers = snitch(get_tender_tenderers) + test_bid_Administrator_change = snitch(bid_Administrator_change) + + +class TenderBidCriteriaTest(TenderContentWebTest): + initial_status = "active.tendering" + test_criteria = test_criteria_1 + + test_multiple_criterias = snitch( + requirement_response_validation_multiple_criterias + ) + + +class TenderBidCriteriaGroupTest(TenderContentWebTest): + initial_status = "active.tendering" + test_criteria = test_criteria_2 + + test_multiple_groups = snitch( + requirement_response_validation_multiple_groups + ) + + +class TenderBidCriteriaMultipleGroupTest(TenderContentWebTest): + initial_status = "active.tendering" + test_criteria = test_criteria_3 + + test_multiple_groups_multiple_requirements = snitch( + requirement_response_validation_multiple_groups_multiple_requirements + ) + + +class TenderBidCriteriaOneGroupMultipleRequirementsTest(TenderContentWebTest): + initial_status = "active.tendering" + test_criteria = test_criteria_4 + + test_multiple_groups_multiple_requirements = snitch( + requirement_response_validation_one_group_multiple_requirements + ) + + +class TenderBidDocumentResourceTest(TenderContentWebTest): + + initial_status = "active.tendering" + + def setUp(self): + super(TenderBidDocumentResourceTest, self).setUp() + # Create bid + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid}}, + ) + bid = response.json["data"] + self.bid = bid + self.bid_id = bid["id"] + self.bid_token = response.json["access"]["token"] + + test_not_found = snitch(not_found) + test_create_tender_bid_document = snitch(create_tender_bid_document) + test_put_tender_bid_document = snitch(put_tender_bid_document) + test_patch_tender_bid_document = snitch(patch_tender_bid_document) + test_create_tender_bid_document_nopending = snitch(create_tender_bid_document_nopending) + + +class TenderBidDocumentWithDSResourceTest(TenderBidDocumentResourceTest): + docservice = True + + test_create_tender_bid_document_json = snitch(create_tender_bid_document_json) + test_put_tender_bid_document_json = snitch(put_tender_bid_document_json) + + +class TenderBidBatchDocumentWithDSResourceTest(TenderContentWebTest): + docservice = True + initial_status = "active.tendering" + bid_data_wo_docs = {"tenderers": [test_organization], "value": {"amount": 500}, "documents": [], "requirementResponses": test_requirement_response_valid} + + test_create_tender_bid_with_document_invalid = snitch(create_tender_bid_with_document_invalid) + test_create_tender_bid_with_document = snitch(create_tender_bid_with_document) + test_create_tender_bid_with_documents = snitch(create_tender_bid_with_documents) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderBidDocumentResourceTest)) + suite.addTest(unittest.makeSuite(TenderBidDocumentWithDSResourceTest)) + suite.addTest(unittest.makeSuite(TenderBidFeaturesResourceTest)) + suite.addTest(unittest.makeSuite(TenderBidResourceTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/bid_blanks.py b/src/openprocurement/tender/pricequotation/tests/bid_blanks.py new file mode 100644 index 0000000000..f781f29e19 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/bid_blanks.py @@ -0,0 +1,825 @@ +# -*- coding: utf-8 -*- +from copy import deepcopy + +import mock +from datetime import timedelta + +from openprocurement.api.utils import get_now +from openprocurement.tender.pricequotation.tests.data import\ + test_criteria_1, test_criteria_2, test_criteria_3, test_criteria_4 +from openprocurement.tender.pricequotation.tests.base import \ + test_organization, test_requirement_response_valid, test_response_1,\ + test_response_2_1, test_response_2_2, test_response_3_1,\ + test_response_3_2, test_response_4 + + +def create_tender_bid_invalid(self): + response = self.app.post_json( + "/tenders/some_id/bids", {"data": {"tenderers": [test_organization], "value": {"amount": 500}}}, status=404 + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + request_path = "/tenders/{}/bids".format(self.tender_id) + response = self.app.post(request_path, "data", status=415) + self.assertEqual(response.status, "415 Unsupported Media Type") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": u"Content-Type header should be one of ['application/json']", + u"location": u"header", + u"name": u"Content-Type", + } + ], + ) + + response = self.app.post(request_path, "data", content_type="application/json", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": u"No JSON object could be decoded", u"location": u"body", u"name": u"data"}], + ) + + response = self.app.post_json(request_path, "data", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"not_data": {}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"data": {"invalid_field": "invalid_value"}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Rogue field", u"location": u"body", u"name": u"invalid_field"}] + ) + + response = self.app.post_json(request_path, {"data": {"tenderers": [{"identifier": "invalid_value"}]}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": { + u"identifier": [u"Please use a mapping for this field or Identifier instance instead of unicode."] + }, + u"location": u"body", + u"name": u"tenderers", + } + ], + ) + + response = self.app.post_json(request_path, {"data": {"tenderers": [{"identifier": {}}], "requirementResponses": test_requirement_response_valid}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [ + { + u"contactPoint": [u"This field is required."], + u"identifier": {u"scheme": [u"This field is required."], u"id": [u"This field is required."]}, + u"name": [u"This field is required."], + u"address": [u"This field is required."], + } + ], + u"location": u"body", + u"name": u"tenderers", + } + ], + ) + + response = self.app.post_json( + request_path, {"data": {"tenderers": [{"name": "name", "identifier": {"uri": "invalid_value"}}], "requirementResponses": test_requirement_response_valid}}, status=422 + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [ + { + u"contactPoint": [u"This field is required."], + u"identifier": { + u"scheme": [u"This field is required."], + u"id": [u"This field is required."], + u"uri": [u"Not a well formed URL."], + }, + u"address": [u"This field is required."], + } + ], + u"location": u"body", + u"name": u"tenderers", + } + ], + ) + + response = self.app.post_json(request_path, {"data": {"tenderers": [test_organization], "requirementResponses": test_requirement_response_valid}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": [u"This field is required."], u"location": u"body", u"name": u"value"}], + ) + + response = self.app.post_json( + request_path, + {"data": {"tenderers": [test_organization], "value": {"amount": 500, "valueAddedTaxIncluded": False}, "requirementResponses": test_requirement_response_valid}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [ + u"valueAddedTaxIncluded of bid should be identical to valueAddedTaxIncluded of value of tender" + ], + u"location": u"body", + u"name": u"value", + } + ], + ) + + response = self.app.post_json( + request_path, + {"data": {"tenderers": [test_organization], "value": {"amount": 500, "currency": "USD"}, "requirementResponses": test_requirement_response_valid}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [u"currency of bid should be identical to currency of value of tender"], + u"location": u"body", + u"name": u"value", + } + ], + ) + + response = self.app.post_json( + request_path, {"data": {"tenderers": test_organization, "value": {"amount": 500}}}, status=422 + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertIn(u"invalid literal for int() with base 10", response.json["errors"][0]["description"]) + + response = self.app.post_json( + request_path, {"data": {"tenderers": [test_organization], "value": {"amount": 500}}}, status=422 + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": [u"This field is required."], u"location": u"body", u"name": u"requirementResponses"}], + ) + + +def create_tender_bid(self): + dateModified = self.db.get(self.tender_id).get("dateModified") + + # Revert tender to statuses ('draft', 'draft.unsuccessful', 'draft.publishing') + data = self.db.get(self.tender_id) + current_status = data.get('status') + criteria = data.pop('criteria') + + for status in ('draft', 'draft.publishing', 'draft.unsuccessful'): + data['status'] = status + self.db.save(data) + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + { + "data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid + } + }, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json['errors'], + [{"location": "body", + "name": "data", + "description": "Can't add bid in current ({}) tender status".format(status)}]) + + # Restore tender to 'active.tendering' status + data['status'] = current_status + data['criteria'] = criteria + self.db.save(data) + + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + bid = response.json["data"] + self.assertEqual(bid["tenderers"][0]["name"], test_organization["name"]) + self.assertIn("id", bid) + self.assertIn(bid["id"], response.headers["Location"]) + + self.assertEqual(self.db.get(self.tender_id).get("dateModified"), dateModified) + + self.set_status("complete") + + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Can't add bid in current (complete) tender status") + + +def requirement_response_validation_multiple_criterias(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response_1 + }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + test_response = deepcopy(test_response_1) + test_response[0]['value'] = 'ivalid' + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response + }}, + status=422 + ) + self.assertEqual(response.status, '422 Unprocessable Entity') + self.assertEqual(response.content_type, "application/json") + data = response.json + self.assertEqual(data['status'], "error") + self.assertEqual( + data['errors'], [{ + u'description': [u'Value "ivalid" does not match expected value "Розчин для інфузій" in reqirement 400496-0001-001-01'], + u'location': u'body', + u'name': u'requirementResponses' + }] + ) + + test_response = deepcopy(test_response_1) + test_response[1]['value'] = '4' + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response + }}, + status=422 + ) + self.assertEqual(response.status, '422 Unprocessable Entity') + self.assertEqual(response.content_type, "application/json") + data = response.json + self.assertEqual(data['status'], "error") + self.assertEqual( + data['errors'], [{ + u'description': [u'Value 4 is lower then minimal required 5 in reqirement 400496-0002-001-01'], + u'location': u'body', + u'name': u'requirementResponses' + }] + ) + + test_response = deepcopy(test_response_1) + test_response.pop() + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response + }}, + status=422 + ) + self.assertEqual(response.status, '422 Unprocessable Entity') + self.assertEqual(response.content_type, "application/json") + data = response.json + self.assertEqual(data['status'], "error") + self.assertEqual( + data['errors'], [{ + u'description': [u"Missing references for criterias: [u'400496-0002']"], + u'location': u'body', + u'name': u'requirementResponses' + }] + ) + + +def requirement_response_validation_multiple_groups(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response_2_1 + }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response_2_2 + }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + test_response = deepcopy(test_response_2_2) + test_response.extend(test_response_2_1) + + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response + }}, + status=422 + ) + self.assertEqual(response.status, '422 Unprocessable Entity') + self.assertEqual(response.content_type, "application/json") + data = response.json + self.assertEqual(data['status'], "error") + self.assertEqual( + data['errors'], [{ + u'description': [u"Provided groups [u'400496-0001-002', u'400496-0001-001'] conflicting in criteria 400496-0001"], + u'location': u'body', + u'name': u'requirementResponses' + }] + ) + + +def requirement_response_validation_multiple_groups_multiple_requirements(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response_3_1 + }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response_3_2 + }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + test_response = deepcopy(test_response_3_1) + test_response.extend(test_response_3_2) + + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response + }}, + status=422 + ) + self.assertEqual(response.status, '422 Unprocessable Entity') + self.assertEqual(response.content_type, "application/json") + data = response.json + self.assertEqual(data['status'], "error") + self.assertEqual( + data['errors'], [{ + u'description': [u"Provided groups [u'400496-0001-002', u'400496-0001-001'] conflicting in criteria 400496-0001"], + u'location': u'body', + u'name': u'requirementResponses' + }] + ) + + +def requirement_response_validation_one_group_multiple_requirements(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response_4 + }}, + status=422 + ) + self.assertEqual(response.status, '422 Unprocessable Entity') + self.assertEqual(response.content_type, "application/json") + data = response.json + self.assertEqual(data['status'], "error") + self.assertEqual( + data['errors'], [{ + u'description': [u'Value "Порошок" does not match expected value "Розчин" in reqirement 400496-0001-001-01'], + u'location': u'body', + u'name': u'requirementResponses' + }] + ) + + test_response = deepcopy(test_response_4) + test_response[0]['value'] = u'Розчин' + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_response + }}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + +def patch_tender_bid(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "status": "draft", "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + bid = response.json["data"] + token = response.json["access"]["token"] + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), + {"data": {"value": {"amount": 60000}}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [u"value of bid should be less than value of tender"], + u"location": u"body", + u"name": u"value", + } + ], + ) + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), + {"data": {"tenderers": [{"name": u"Державне управління управлінням справами"}]}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["date"], bid["date"]) + self.assertNotEqual(response.json["data"]["tenderers"][0]["name"], bid["tenderers"][0]["name"]) + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), + {"data": {"value": {"amount": 500}, "tenderers": [test_organization]}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["date"], bid["date"]) + self.assertEqual(response.json["data"]["tenderers"][0]["name"], bid["tenderers"][0]["name"]) + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), + {"data": {"value": {"amount": 400}}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["value"]["amount"], 400) + self.assertNotEqual(response.json["data"]["date"], bid["date"]) + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), {"data": {"status": "active"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "active") + self.assertNotEqual(response.json["data"]["date"], bid["date"]) + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), + {"data": {"status": "draft"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Can't update bid to (draft) status") + + response = self.app.patch_json( + "/tenders/{}/bids/some_id".format(self.tender_id), {"data": {"value": {"amount": 400}}}, status=404 + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"bid_id"}]) + + response = self.app.patch_json("/tenders/some_id/bids/some_id", {"data": {"value": {"amount": 400}}}, status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + self.set_status("complete") + + response = self.app.get("/tenders/{}/bids/{}".format(self.tender_id, bid["id"])) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["value"]["amount"], 400) + + response = self.app.patch_json( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), + {"data": {"value": {"amount": 400}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Can't update bid in current (complete) tender status") + + +def get_tender_bid(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + bid = response.json["data"] + bid_token = response.json["access"]["token"] + + response = self.app.get("/tenders/{}/bids/{}".format(self.tender_id, bid["id"]), status=403) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't view bid in current (active.tendering) tender status" + ) + + response = self.app.get("/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], bid_token)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"], bid) + + self.set_status("active.qualification") + + response = self.app.get("/tenders/{}/bids/{}".format(self.tender_id, bid["id"])) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + bid_data = response.json["data"] + self.assertEqual(bid_data, bid) + + response = self.app.get("/tenders/{}/bids/some_id".format(self.tender_id), status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"bid_id"}]) + + response = self.app.get("/tenders/some_id/bids/some_id", status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + response = self.app.delete( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], bid_token), status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't delete bid in current (active.qualification) tender status" + ) + + +def delete_tender_bid(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + bid = response.json["data"] + bid_token = response.json["access"]["token"] + + response = self.app.delete( + "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], bid_token), + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], 'error') + self.assertEqual(response.json["errors"], [ + {"location": "body", "name": "data", "description": "Can't delete bid in Price Quotation tender"} + ]) + + response = self.app.delete("/tenders/{}/bids/some_id".format(self.tender_id), status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"bid_id"}]) + + response = self.app.delete("/tenders/some_id/bids/some_id", status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + +def get_tender_tenderers(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + bid = response.json["data"] + + response = self.app.get("/tenders/{}/bids".format(self.tender_id), status=403) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't view bids in current (active.tendering) tender status" + ) + + self.set_status("active.qualification") + + response = self.app.get("/tenders/{}/bids".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"][0], bid) + + response = self.app.get("/tenders/some_id/bids", status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + +def bid_Administrator_change(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + bid = response.json["data"] + + self.app.authorization = ("Basic", ("administrator", "")) + response = self.app.patch_json( + "/tenders/{}/bids/{}".format(self.tender_id, bid["id"]), + {"data": {"tenderers": [{"identifier": {"id": "00000000"}}], "value": {"amount": 400}}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertNotEqual(response.json["data"]["value"]["amount"], 400) + self.assertEqual(response.json["data"]["tenderers"][0]["identifier"]["id"], "00000000") + + +def patch_tender_bid_document(self): + response = self.app.post( + "/tenders/{}/bids/{}/documents?acc_token={}".format(self.tender_id, self.bid_id, self.bid_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + + response = self.app.patch_json( + "/tenders/{}/bids/{}/documents/{}?acc_token={}".format(self.tender_id, self.bid_id, doc_id, self.bid_token), + {"data": {"description": "document description"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + + response = self.app.get( + "/tenders/{}/bids/{}/documents/{}?acc_token={}".format(self.tender_id, self.bid_id, doc_id, self.bid_token) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("document description", response.json["data"]["description"]) + + self.set_status("active.awarded") + + response = self.app.patch_json( + "/tenders/{}/bids/{}/documents/{}?acc_token={}".format(self.tender_id, self.bid_id, doc_id, self.bid_token), + {"data": {"description": "document description"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update document in current (active.awarded) tender status" + ) + + +def create_tender_bid_document_nopending(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": {"tenderers": [test_organization], "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid}}, + ) + bid = response.json['data'] + token = response.json['access']['token'] + bid_id = bid['id'] + + response = self.app.post( + "/tenders/{}/bids/{}/documents?acc_token={}".format(self.tender_id, bid_id, token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + self.set_status("active.tendering", 'end') + response = self.check_chronograph() + self.assertEqual(response.json["data"]["status"], "active.qualification") + response = self.app.patch_json( + "/tenders/{}/bids/{}/documents/{}?acc_token={}".format(self.tender_id, bid_id, doc_id, token), + {"data": {"description": "document description"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update document because award of bid is not in pending state" + ) + + response = self.app.put( + "/tenders/{}/bids/{}/documents/{}?acc_token={}".format(self.tender_id, bid_id, doc_id, token), + "content3", + content_type="application/msword", + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update document because award of bid is not in pending state" + ) + + response = self.app.post( + "/tenders/{}/bids/{}/documents?acc_token={}".format(self.tender_id, bid_id, token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't add document because award of bid is not in pending state" + ) diff --git a/src/openprocurement/tender/pricequotation/tests/cancellation.py b/src/openprocurement/tender/pricequotation/tests/cancellation.py new file mode 100644 index 0000000000..f3fa0d9acc --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/cancellation.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +import unittest + +from openprocurement.api.tests.base import snitch + +from openprocurement.tender.pricequotation.tests.base import ( + TenderContentWebTest, test_bids, + test_cancellation, +) +from openprocurement.tender.belowthreshold.tests.cancellation import\ + TenderCancellationDocumentResourceTestMixin +from openprocurement.tender.belowthreshold.tests.cancellation_blanks import ( + get_tender_cancellation, + get_tender_cancellations, + ) +from openprocurement.tender.pricequotation.tests.cancellation_blanks import ( + create_tender_cancellation, + create_tender_cancellation_invalid, + patch_tender_cancellation, +) + + +class TenderCancellationResourceTestMixin(object): + initial_status = 'active.tendering' + + test_create_tender_cancellation_invalid = snitch(create_tender_cancellation_invalid) + test_create_tender_cancellation = snitch(create_tender_cancellation) + test_patch_tender_cancellation = snitch(patch_tender_cancellation) + test_get_tender_cancellation = snitch(get_tender_cancellation) + test_get_tender_cancellations = snitch(get_tender_cancellations) + + + + +class TenderCancellationActiveTenderingResourceTest( + TenderContentWebTest, + TenderCancellationResourceTestMixin, +): + initial_status = "active.tendering" + initial_bids = test_bids + valid_reasonType_choices = ["noDemand", "unFixable", "expensesCut"] + + @property + def tender_token(self): + data = self.db.get(self.tender_id) + return data['owner_token'] + + +class TenderCancellationActiveQualificationResourceTest(TenderCancellationActiveTenderingResourceTest): + initial_status = "active.qualification" + initial_bids = test_bids + valid_reasonType_choices = ["noDemand", "unFixable", "expensesCut"] + + +class TenderCancellationActiveAwardedResourceTest(TenderCancellationActiveTenderingResourceTest): + initial_status = "active.awarded" + initial_bids = test_bids + valid_reasonType_choices = ["noDemand", "unFixable", "expensesCut"] + + +class TenderCancellationDocumentResourceTest(TenderContentWebTest, TenderCancellationDocumentResourceTestMixin): + def setUp(self): + super(TenderCancellationDocumentResourceTest, self).setUp() + # Create cancellation + cancellation = dict(**test_cancellation) + cancellation.update({ + "reasonType": "noDemand" + }) + + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), + {"data": cancellation}, + ) + cancellation = response.json["data"] + self.cancellation_id = cancellation["id"] + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderCancellationDocumentResourceTest)) + suite.addTest(unittest.makeSuite(TenderCancellationActiveTenderingResourceTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/cancellation_blanks.py b/src/openprocurement/tender/pricequotation/tests/cancellation_blanks.py new file mode 100644 index 0000000000..299c0ea796 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/cancellation_blanks.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +from openprocurement.tender.pricequotation.tests.base import test_cancellation + + +def create_tender_cancellation_invalid(self): + cancellation = dict(**test_cancellation) + response = self.app.post_json( + "/tenders/some_id/cancellations", {"data": cancellation}, status=404 + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + request_path = "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token) + + response = self.app.post(request_path, "data", status=415) + self.assertEqual(response.status, "415 Unsupported Media Type") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": u"Content-Type header should be one of ['application/json']", + u"location": u"header", + u"name": u"Content-Type", + } + ], + ) + + response = self.app.post(request_path, "data", content_type="application/json", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": u"No JSON object could be decoded", u"location": u"body", u"name": u"data"}], + ) + + response = self.app.post_json(request_path, "data", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"not_data": {}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"data": {}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + {u"description": [u"This field is required."], u"location": u"body", u"name": u"reason"}, + ], + ) + + response = self.app.post_json(request_path, {"data": {"invalid_field": "invalid_value"}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Rogue field", u"location": u"body", u"name": u"invalid_field"}] + ) + + + +def create_tender_cancellation(self): + cancellation = dict(**test_cancellation) + + request_path = "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token) + response = self.app.post_json(request_path, {"data": cancellation}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + cancellation = response.json["data"] + self.assertEqual(cancellation["reason"], "cancellation reason") + self.assertIn("id", cancellation) + self.assertIn("date", cancellation) + self.assertIn(cancellation["id"], response.headers["Location"]) + + response = self.app.get("/tenders/{}".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], self.initial_status) + response = self.app.post( + "/tenders/{}/cancellations/{}/documents?acc_token={}".format( + self.tender_id, cancellation["id"], self.tender_token + ), + upload_files=[("file", "name.doc", "content")], + ) + + cancellation.update({ + "status": "active" + }) + response = self.app.patch_json( + "/tenders/{}/cancellations/{}?acc_token={}".format(self.tender_id, cancellation['id'], self.tender_token), + {"data": cancellation}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + cancellation = response.json["data"] + self.assertEqual(cancellation["reason"], "cancellation reason") + self.assertEqual(cancellation["status"], "active") + self.assertIn("id", cancellation) + + response = self.app.get("/tenders/{}".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "cancelled") + if self.initial_status == "active.tendering": + self.assertNotIn("bids", response.json["data"]) + + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), + {"data": test_cancellation}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update tender in current (cancelled) status" + ) + + + +def patch_tender_cancellation(self): + tender = self.app.get('/tenders/{}'.format(self.tender_id)).json['data'] + status = tender['status'] + cancellation = dict(**test_cancellation) + + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), + {"data": cancellation}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + cancellation = response.json["data"] + + response = self.app.post( + "/tenders/{}/cancellations/{}/documents?acc_token={}".format( + self.tender_id, cancellation['id'], self.tender_token + ), + upload_files=[("file", "name.doc", "content")], + ) + + + response = self.app.patch_json( + "/tenders/{}/cancellations/{}?acc_token={}".format(self.tender_id, cancellation["id"], self.tender_token), + {"data": {"status": "active"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "active") + + response = self.app.get("/tenders/{}".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "cancelled") + if status == 'active.tendering': + self.assertNotIn("bids", response.json["data"]) + + response = self.app.patch_json( + "/tenders/{}/cancellations/some_id".format(self.tender_id), {"data": {"status": "active"}}, status=404 + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"cancellation_id"}] + ) + + response = self.app.patch_json("/tenders/some_id/cancellations/some_id", {"data": {"status": "active"}}, status=404) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + response = self.app.get("/tenders/{}/cancellations/{}".format(self.tender_id, cancellation["id"])) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "active") + self.assertEqual(response.json["data"]["reason"], "cancellation reason") diff --git a/src/openprocurement/tender/pricequotation/tests/chronograph.py b/src/openprocurement/tender/pricequotation/tests/chronograph.py new file mode 100644 index 0000000000..78a9fa03fb --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/chronograph.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import unittest + +from openprocurement.api.tests.base import snitch + +from openprocurement.tender.pricequotation.tests.base import TenderContentWebTest +from openprocurement.tender.pricequotation.tests.chronograph_blanks import ( + switch_to_qualification, + switch_to_unsuccessful, +) + + +class TenderChronographResourceTest(TenderContentWebTest): + initial_status = "active.tendering" + + test_switch_to_qualification = snitch(switch_to_qualification) + test_switch_to_unsuccessful = snitch(switch_to_unsuccessful) + + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderChronographResourceTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/chronograph_blanks.py b/src/openprocurement/tender/pricequotation/tests/chronograph_blanks.py new file mode 100644 index 0000000000..34fff2fd4c --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/chronograph_blanks.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from openprocurement.tender.pricequotation.tests.data import ( + test_organization, + test_requirement_response_valid +) + + +# TenderSwitchQualificationResourceTest +def switch_to_qualification(self): + response = self.app.post_json( + "/tenders/{}/bids".format(self.tender_id), + {"data": { + "tenderers": [test_organization], "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid + }}, + ) + + bid = response.json["data"] + bid_id = bid["id"] + self.set_status("active.tendering", 'end') + + response = self.check_chronograph() + self.assertEqual(response.json["data"]["status"], "active.qualification") + self.assertEqual(len(response.json["data"]["awards"]), 1) + self.assertEqual(response.json["data"]["awards"][0]['bid_id'], bid_id) + + +# TenderSwitchUnsuccessfulResourceTest +def switch_to_unsuccessful(self): + self.set_status("active.tendering", 'end') + response = self.check_chronograph() + self.assertEqual(response.json["data"]["status"], "unsuccessful") + if self.initial_lots: + self.assertEqual( + set([i["status"] for i in response.json["data"]["lots"]]), + set(["unsuccessful"]) + ) diff --git a/src/openprocurement/tender/pricequotation/tests/contract.py b/src/openprocurement/tender/pricequotation/tests/contract.py new file mode 100644 index 0000000000..4102a1c646 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/contract.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +import unittest +from openprocurement.api.tests.base import snitch +from openprocurement.tender.pricequotation.tests.base import ( + TenderContentWebTest, + test_bids, + ) +from openprocurement.tender.belowthreshold.tests.contract import ( + TenderContractResourceTestMixin, + TenderContractDocumentResourceTestMixin + ) +from openprocurement.tender.belowthreshold.tests.contract_blanks import ( + create_tender_contract, + create_tender_contract_in_complete_status, + patch_tender_contract_value, + ) +from openprocurement.tender.pricequotation.tests.contract_blanks import ( + patch_tender_contract, + patch_tender_contract_value_vat_not_included, + ) + + +class TenderContractResourceTest(TenderContentWebTest, + TenderContractResourceTestMixin): + initial_status = "active.awarded" + initial_bids = test_bids + + def setUp(self): + super(TenderContractResourceTest, self).setUp() + self.award_id = self.award_ids[-1] + resp = self.app.get( + "/tenders/{}/awards/{}".format(self.tender_id, self.award_id), + ) + award = resp.json["data"] + self.award_value = award["value"] + self.award_suppliers = award["suppliers"] + self.award_items = award["items"] + + test_create_tender_contract = snitch(create_tender_contract) + test_create_tender_contract_in_complete_status = snitch( + create_tender_contract_in_complete_status + ) + test_patch_tender_contract = snitch(patch_tender_contract) + test_patch_tender_contract_value = snitch(patch_tender_contract_value) + + +class TenderContractVATNotIncludedResourceTest(TenderContentWebTest, + TenderContractResourceTestMixin): + initial_status = "active.awarded" + initial_bids = test_bids + + def setUp(self): + super(TenderContractVATNotIncludedResourceTest, self).setUp() + self.award_id = self.award_ids[-1] + resp = self.app.get( + "/tenders/{}/awards/{}".format(self.tender_id, self.award_id), + ) + award = resp.json["data"] + self.award_value = award["value"] + self.award_suppliers = award["suppliers"] + self.award_items = award["items"] + + test_patch_tender_contract_value_vat_not_included = snitch( + patch_tender_contract_value_vat_not_included + ) + + +class TenderContractDocumentResourceTest(TenderContentWebTest, + TenderContractDocumentResourceTestMixin): + initial_status = "active.awarded" + initial_bids = test_bids + + def setUp(self): + super(TenderContractDocumentResourceTest, self).setUp() + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderContractResourceTest)) + suite.addTest(unittest.makeSuite(TenderContractDocumentResourceTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/contract_blanks.py b/src/openprocurement/tender/pricequotation/tests/contract_blanks.py new file mode 100644 index 0000000000..e1784b923d --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/contract_blanks.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +from datetime import timedelta +from openprocurement.api.utils import get_now + + +def patch_tender_contract(self): + self.app.authorization = ("Basic", ("token", "")) + response = self.app.get("/tenders/{}/contracts".format(self.tender_id)) + contract = response.json["data"][0] + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"amountNet": contract["value"]["amount"] - 1}}}, + ) + self.assertEqual(response.status, "200 OK") + + self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + { + "data": { + "contractID": "myselfID", + "items": [{"description": "New Description"}], + "suppliers": [{"name": "New Name"}], + } + }, + ) + + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, contract["id"])) + self.assertEqual(response.json["data"]["contractID"], contract["contractID"]) + self.assertEqual(response.json["data"]["items"], contract["items"]) + self.assertEqual(response.json["data"]["suppliers"], contract["suppliers"]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"currency": "USD"}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], "Can't update currency for contract value") + + one_hour_in_furure = (get_now() + timedelta(hours=1)).isoformat() + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"dateSigned": one_hour_in_furure}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [u"Contract signature date can't be in the future"], + u"location": u"body", + u"name": u"dateSigned", + } + ], + ) + + custom_signature_date = get_now().isoformat() + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"dateSigned": custom_signature_date}}, + ) + self.assertEqual(response.status, "200 OK") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"status": "active"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "active") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"amount": 232}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update contract in current (complete) tender status" + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"contractID": "myselfID"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update contract in current (complete) tender status" + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"items": [{"description": "New Description"}]}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update contract in current (complete) tender status" + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"suppliers": [{"name": "New Name"}]}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update contract in current (complete) tender status" + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"status": "active"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update contract in current (complete) tender status" + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/some_id?acc_token={}".format(self.tender_id, self.tender_token), + {"data": {"status": "active"}}, + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"contract_id"}] + ) + + response = self.app.patch_json( + "/tenders/some_id/contracts/some_id?acc_token={}".format(self.tender_token), + {"data": {"status": "active"}}, + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] + ) + + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, contract["id"])) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["status"], "active") + self.assertEqual(response.json["data"]["contractID"], contract["contractID"]) + self.assertEqual(response.json["data"]["items"], contract["items"]) + self.assertEqual(response.json["data"]["suppliers"], contract["suppliers"]) + self.assertEqual(response.json["data"]["dateSigned"], custom_signature_date) + + +def patch_tender_contract_value_vat_not_included(self): + response = self.app.get("/tenders/{}/contracts".format(self.tender_id)) + contract = response.json["data"][0] + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"valueAddedTaxIncluded": False, 'amountNet': contract['value']['amount']}}}, + ) + self.assertEqual(response.status, "200 OK") + + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"currency": "USD"}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], "Can't update currency for contract value") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"amount": 467}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], "Amount and amountNet should be equal") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"amount": 22600, "amountNet": 22600}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], "Amount should be less or equal to awarded amount") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"amount": 400, "amountNet": 400}}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["value"]["amount"], 400) + self.assertEqual(response.json["data"]["value"]["amountNet"], 400) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), + {"data": {"value": {"valueAddedTaxIncluded": True}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"][0]["description"], + "Amount should be greater than amountNet and differ by no more than 20.0%", + ) diff --git a/src/openprocurement/tender/pricequotation/tests/data.py b/src/openprocurement/tender/pricequotation/tests/data.py new file mode 100644 index 0000000000..3a4819b473 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/data.py @@ -0,0 +1,758 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from copy import deepcopy +from datetime import datetime, timedelta +from openprocurement.api.utils import get_now +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.api.constants import SANDBOX_MODE, RELEASE_2020_04_19 + + +now = get_now() + + +PERIODS = { + "active.tendering": { + "start": { + "tenderPeriod": { + "startDate": -timedelta(), + "endDate": timedelta(days=5) + }, + }, + "end": { + "tenderPeriod": { + "startDate": - timedelta(days=5), + "endDate": timedelta() + }, + }, + }, + "active.qualification": { + "start": { + "tenderPeriod": { + "startDate": - timedelta(days=10), + "endDate": - timedelta(days=1), + }, + "awardPeriod": {"startDate": timedelta()}, + }, + "end": { + "tenderPeriod": { + "startDate": - timedelta(days=10), + "endDate": - timedelta(days=1), + }, + "awardPeriod": {"startDate": timedelta()}, + }, + }, + "active.awarded": { + "start": { + "tenderPeriod": { + "startDate": - timedelta(days=10), + "endDate": - timedelta(days=1), + }, + "awardPeriod": {"startDate": timedelta(), "endDate": timedelta()}, + }, + "end": { + "tenderPeriod": { + "startDate": - timedelta(days=10), + "endDate": - timedelta(days=2), + }, + "awardPeriod": { + "startDate": - timedelta(days=1), + "endDate": - timedelta(days=1), + }, + }, + }, + "complete": { + "start": { + "tenderPeriod": { + "startDate": - timedelta(days=10), + "endDate": - timedelta(days=1) + }, + "awardPeriod": { + "startDate": - timedelta(days=1), + "endDate": -timedelta(), + }, + } + }, +} + + +test_requirement_response_valid = [ + { + "value": "23.8", + 'requirement': { + 'id': "655360-0001-001-01" + } + }, + { + "value": "1920x1080", + 'requirement': { + 'id': "655360-0002-001-01" + } + }, + { + "value": "16:9", + 'requirement': { + 'id': "655360-0003-001-01" + } + }, + { + "value": 250, + 'requirement': { + 'id': "655360-0004-001-01" + } + }, + { + "value": "1000:1", + 'requirement': { + 'id': "655360-0005-001-01" + } + }, + { + "value": 1, + 'requirement': { + 'id': "655360-0006-001-01" + } + }, + { + "value": 1, + 'requirement': { + 'id': "655360-0007-001-01" + } + }, + { + "value": "HDMI", + 'requirement': { + 'id': "655360-0008-001-01" + } + }, + { + "value": 36, + 'requirement': { + 'id': "655360-0009-001-01" + } + } +] + +test_organization = { + "name": u"Державне управління справами", + "identifier": {"scheme": u"UA-EDR", "id": u"00037256", "uri": u"http://www.dus.gov.ua/"}, + "address": { + "countryName": u"Україна", + "postalCode": u"01220", + "region": u"м. Київ", + "locality": u"м. Київ", + "streetAddress": u"вул. Банкова, 11, корпус 1", + }, + "contactPoint": {"name": u"Державне управління справами", "telephone": u"0440000000"}, + "scale": "micro", +} + + +test_milestones = [ + { + "id": "a" * 32, + "title": "signingTheContract", + "code": "prepayment", + "type": "financing", + "duration": {"days": 2, "type": "banking"}, + "sequenceNumber": 0, + "percentage": 45.55, + }, + { + "title": "deliveryOfGoods", + "code": "postpayment", + "type": "financing", + "duration": {"days": 900, "type": "calendar"}, + "sequenceNumber": 0, + "percentage": 54.45, + }, +] + +test_author = test_organization.copy() +del test_author["scale"] + +test_procuringEntity = test_author.copy() +test_procuringEntity["kind"] = "general" + +test_item = { + "description": u"Комп’ютерне обладнання", + "classification": {"scheme": u"ДК021", "id": u"44617100-9", "description": u"Cartons"}, + "additionalClassifications": [ + {"scheme": u"INN", "id": u"17.21.1", "description": u"папір і картон гофровані, паперова й картонна тара"} + ], + "quantity": 1, + "deliveryDate": { + "startDate": (now + timedelta(days=2)).isoformat(), + "endDate": (now + timedelta(days=5)).isoformat(), + }, + "deliveryAddress": { + "countryName": u"Україна", + "postalCode": "79000", + "region": u"м. Київ", + "locality": u"м. Київ", + "streetAddress": u"вул. Банкова 1", + }, +} + +test_tender_data = { + "title": u"Комп’ютерне обладнання", + "profile": "655360-30230000-889652-40000777", + "mainProcurementCategory": "goods", + "procuringEntity": test_procuringEntity, + "items": [deepcopy(test_item)], + "value": {"amount": 22000, "currency": "UAH"}, + "tenderPeriod": {"endDate": (now + timedelta(days=14)).isoformat()}, + "procurementMethodType": PMT, + "procurementMethod": 'selective', +} +if SANDBOX_MODE: + test_tender_data["procurementMethodDetails"] = "quick, accelerator=1440" + +test_bids = [ + {"tenderers": [test_organization], "value": {"amount": 469, "currency": "UAH", "valueAddedTaxIncluded": True}, "requirementResponses": test_requirement_response_valid}, + {"tenderers": [test_organization], "value": {"amount": 479, "currency": "UAH", "valueAddedTaxIncluded": True}, "requirementResponses": test_requirement_response_valid}, +] +bid_with_docs = deepcopy(test_bids[1]) +bid_with_docs["documents"] = [ + { + 'title': u'Proposal_part1.pdf', + 'url': u"http://broken1.ds", + 'hash': 'md5:' + '0' * 32, + 'format': 'application/pdf', + }, + { + 'title': u'Proposal_part2.pdf', + 'url': u"http://broken2.ds", + 'hash': 'md5:' + '0' * 32, + 'format': 'application/pdf', + } +] + +test_cancellation = { + "reason": "cancellation reason", + "reasonType": "noDemand", + "cancellationOf": "tender", +} + + +test_shortlisted_firms = [ + { + "address": { + "countryName": u"Україна", + "locality": u"м.Київ", + "postalCode": "01100", + "region": u"Київська область", + "streetAddress": u"бул.Дружби Народів, 8" + }, + "contactPoint": { + "email": "contact@pixel.pix", + "name": u"Оксана Піксель", + "telephone": "(067) 123-45-67" + }, + "id": "UA-EDR-12345678", + "identifier": { + "id": "12345678", + "legalName": u"Товариство з обмеженою відповідальністю «Пікселі»", + "scheme": "UA-EDR" + }, + "name": u"Товариство з обмеженою відповідальністю «Пікселі»", + "scale": "large", + "status": "active" + }, + { + "address": { + "countryName": u"Україна", + "locality": u"м.Тернопіль", + "postalCode": "46000", + "region": u"Тернопільська область", + "streetAddress": u"вул. Кластерна, 777-К" + }, + "contactPoint": { + "email": "info@shteker.pek", + "name": u"Олег Штекер", + "telephone": "(095) 123-45-67" + }, + "id": "UA-EDR-87654321", + "identifier": { + "id": "87654321", + "legalName": u"Товариство з обмеженою відповідальністю «Штекер-Пекер»", + "scheme": "UA-EDR" + }, + "name": u"Товариство з обмеженою відповідальністю «Штекер-Пекер»", + "scale": "large", + "status": "active" + } +] + +test_short_profile = { + "classification": { + "description": u"Комп’ютерне обладнанн", + "id": "30230000-0", + "scheme": u"ДК021" + }, + "id": "655360-30230000-889652-40000777", + "unit": { + "code": "H87", + "name": u"штук" + }, + "criteria": [ + { + "description": u"Діагональ екрану", + "id": "655360-0001", + "requirementGroups": [ + { + "description": u"Діагональ екрану, не менше 23.8 дюймів", + "id": "655360-0001-001", + "requirements": [ + { + "dataType": "number", + "id": "655360-0001-001-01", + "minValue": "23.8", + "title": u"Діагональ екрану", + "unit": { + "code": "INH", + "name": u"дюйм" + } + } + ] + } + ], + "title": u"Діагональ екрану" + }, + { + "description": u"Роздільна здатність", + "id": "655360-0002", + "requirementGroups": [ + { + "description": u"Роздільна здатність - 1920x1080", + "id": "655360-0002-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": "1920x1080", + "id": "655360-0002-001-01", + "title": u"Роздільна здатність" + } + ] + } + ], + "title": u"Роздільна здатність" + }, + { + "description": u"Співвідношення сторін", + "id": "655360-0003", + "requirementGroups": [ + { + "description": u"Співвідношення сторін", + "id": "655360-0003-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": "16:9", + "id": "655360-0003-001-01", + "title": u"Співвідношення сторін" + } + ] + } + ], + "title": u"Співвідношення сторін" + }, + { + "description": u"Яскравість дисплея", + "id": "655360-0004", + "requirementGroups": [ + { + "description": u"Яскравість дисплея, не менше 250 кд/м²", + "id": "655360-0004-001", + "requirements": [ + { + "dataType": "integer", + "id": "655360-0004-001-01", + "maxValue": 250, + "title": "Яскравість дисплея", + "unit": { + "code": "A24", + "name": u"кд/м²" + } + } + ] + } + ], + "title": u"Яскравість дисплея" + }, + { + "description": u"Контрастність (статична)", + "id": "655360-0005", + "requirementGroups": [ + { + "description": u"Контрастність (статична) - 1000:1", + "id": "655360-0005-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": "1000:1", + "id": "655360-0005-001-01", + "title": u"Контрастність (статична)" + } + ] + }, + { + "description": u"Контрастність (статична) - 3000:1", + "id": "655360-0005-002", + "requirements": [ + { + "dataType": "string", + "expectedValue": "3000:1", + "id": "655360-0005-002-01", + "title": u"Контрастність (статична)" + } + ] + } + ], + "title": u"Контрастність (статична)" + }, + { + "description": u"Кількість портів HDMI", + "id": "655360-0006", + "requirementGroups": [ + { + "description": u"Кількість портів HDMI, не менше 1 шт.", + "id": "655360-0006-001", + "requirements": [ + { + "dataType": "integer", + "id": "655360-0006-001-01", + "minValue": 1, + "title": u"Кількість портів HDMI", + "unit": { + "code": "H87", + "name": u"штук" + } + } + ] + } + ], + "title": u"Кількість портів HDMI" + }, + { + "description": u"Кількість портів D-sub", + "id": "655360-0007", + "requirementGroups": [ + { + "description": u"Кількість портів D-sub, не менше 1 шт.", + "id": "655360-0007-001", + "requirements": [ + { + "dataType": "integer", + "id": "655360-0007-001-01", + "minValue": 1, + "title": u"Кількість портів D-sub", + "unit": { + "code": "H87", + "name": u"штук" + } + } + ] + } + ], + "title": u"Кількість портів D-sub" + }, + { + "description": u"Кабель для під’єднання", + "id": "655360-0008", + "requirementGroups": [ + { + "description": u"Кабель для під’єднання", + "id": "655360-0008-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": "HDMI", + "id": "655360-0008-001-01", + "title": u"Кабель для під’єднання" + } + ] + } + ], + "title": u"Кабель для під’єднання" + }, + { + "description": u"Строк дії гарантії", + "id": "655360-0009", + "requirementGroups": [ + { + "description": u"Гарантія, не менше 36 місяців", + "id": "655360-0009-001", + "requirements": [ + { + "dataType": "integer", + "id": "655360-0009-001-01", + "minValue": 36, + "title": u"Гарантія", + "unit": { + "code": "MON", + "name": u"місяців" + } + } + ] + } + ], + "title": u"Гарантія" + } + ], + "value": { + "amount": 500, + "currency": "UAH", + "valueAddedTaxIncluded": True + } +} + + +test_criteria_1 = [ + { + "description": u"Форма випуску", + "id": "400496-0001", + "requirementGroups": [ + { + "description": u"Форма випуску", + "id": "400496-0001-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": u"Розчин для інфузій", + "id": "400496-0001-001-01", + "title": u"Форма випуску" + } + ] + } + ], + "title": u"Форма випуску" + }, + { + "description": u"Доза діючої речовини", + "id": "400496-0002", + "requirementGroups": [ + { + "description": u"Доза діючої речовини", + "id": "400496-0002-001", + "requirements": [ + { + "dataType": "integer", + "minValue": 5, + "id": "400496-0002-001-01", + "title": u"Доза діючої речовини", + "unit": { + "code": "GL", + "name": "г/л" + } + } + ] + } + ], + "title": u"Доза діючої речовини" + } +] + +test_criteria_2 = [ + { + "description": u"Форма випуску", + "id": "400496-0001", + "requirementGroups": [ + { + "description": u"Форма випуску", + "id": "400496-0001-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": u"Розчин", + "id": "400496-0001-001-01", + "title": u"Форма випуску" + } + ] + }, + { + "description": u"Форма випуску", + "id": "400496-0001-002", + "requirements": [ + { + "dataType": "string", + "expectedValue": u"Порошок", + "id": "400496-0001-002-01", + "title": u"Форма випуску" + } + ] + } + ], + "title": u"Форма випуску" + } +] + + +test_criteria_3 = [ + { + "description": u"Форма випуску", + "id": "400496-0001", + "requirementGroups": [ + { + "description": u"Форма випуску", + "id": "400496-0001-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": u"Розчин", + "id": "400496-0001-001-01", + "title": u"Форма випуску" + }, + { + "dataType": "integer", + "expectedValue": 500, + "id": "400496-0001-001-02", + "title": u"Форма випуску", + "unit": { + "code": "MLT", + "name": u"мл" + } + } + ] + }, + { + "description": u"Форма випуску", + "id": "400496-0001-002", + "requirements": [ + { + "dataType": "string", + "expectedValue": u"Порошок", + "id": "400496-0001-002-01", + "title": u"Форма випуску" + } + ] + } + ], + "title": u"Форма випуску" + }] + + +test_criteria_4 = [ + { + "description": u"Форма випуску", + "title": u"Форма випуску", + "id": "400496-0001", + "requirementGroups": [ + { + "description": u"Форма випуску", + "id": "400496-0001-001", + "requirements": [ + { + "dataType": "string", + "expectedValue": u"Розчин", + "id": "400496-0001-001-01", + "title": u"Форма випуску" + }, + { + "dataType": "integer", + "expectedValue": 500, + "id": "400496-0001-001-02", + "title": u"Форма випуску", + "unit": { + "code": "MLT", + "name": u"мл" + }, + }, + { + "dataType": "integer", + "expectedValue": 1, + "id": "400496-0001-001-03", + "title": u"Форма випуску", + "unit": { + "code": "H87", + "name": u"ШТ" + } + } + ] + } + ] + } +] + + +test_response_1 = [ + { + "requirement": { + "id": "400496-0001-001-01" + }, + "value": u"Розчин для інфузій" + }, + { + "requirement": { + "id": "400496-0002-001-01" + }, + "value": 5 + } +] + + +test_response_2_1 = [ + { + "requirement": { + "id": "400496-0001-001-01" + }, + "value": u"Розчин" + } +] + + +test_response_2_2 = [ + { + "requirement": { + "id": "400496-0001-002-01" + }, + "value": u"Порошок" + } +] + + +test_response_3_1 = [ + { + "requirement": { + "id": "400496-0001-001-01" + }, + "value": u"Розчин" + }, + { + "requirement": { + "id": "400496-0001-001-02" + }, + "value": 500 + } +] + + +test_response_3_2 = [ + { + "requirement": { + "id": "400496-0001-002-01" + }, + "value": u"Порошок" + } +] + + +test_response_4 = [ + { + "requirement": { + "id": "400496-0001-001-01" + }, + "value": u"Порошок" + }, + { + "requirement": { + "id": "400496-0001-001-02" + }, + "value": 500 + }, + { + "requirement": { + "id": "400496-0001-001-03" + }, + "value": 1 + } +] diff --git a/src/openprocurement/tender/pricequotation/tests/document.py b/src/openprocurement/tender/pricequotation/tests/document.py new file mode 100644 index 0000000000..43595f9b56 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/document.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +import unittest + +from openprocurement.api.tests.base import snitch + +from openprocurement.tender.pricequotation.tests.base import TenderContentWebTest +from openprocurement.tender.belowthreshold.tests.document import ( + TenderDocumentResourceTestMixin, + TenderDocumentWithDSResourceTestMixin +) +from openprocurement.tender.belowthreshold.tests.document_blanks import\ + create_tender_document_error +from openprocurement.tender.pricequotation.tests.document_blanks import ( + create_document_active_tendering_status, +) + + +class TenderDocumentResourceTest(TenderContentWebTest, TenderDocumentResourceTestMixin): + """""" + test_create_document_active_tendering_status = snitch(create_document_active_tendering_status) + + +class TenderDocumentWithDSResourceTest(TenderDocumentResourceTest, TenderDocumentWithDSResourceTestMixin): + docservice = True + + test_create_tender_document_error = snitch(create_tender_document_error) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderDocumentResourceTest)) + suite.addTest(unittest.makeSuite(TenderDocumentWithDSResourceTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/document_blanks.py b/src/openprocurement/tender/pricequotation/tests/document_blanks.py new file mode 100644 index 0000000000..55c17762b9 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/document_blanks.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from email.header import Header + +# TenderDocumentResourceTest +from mock import patch +from openprocurement.tender.core.tests.base import bad_rs_request, srequest + + +def create_document_active_tendering_status(self): + + self.set_status("active.tendering") + # TODO: check if document should not be updated in this |\ status, + # because now there is no status validation + + response = self.app.post( + "/tenders/{}/documents?acc_token={}".format(self.tender_id, self.tender_token), + upload_files=[("file", u"укр.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + # self.assertEqual( + # response.json["errors"][0]["description"], "Can't add document in current (active.tendering) tender status" + # ) diff --git a/src/openprocurement/tender/pricequotation/tests/main.py b/src/openprocurement/tender/pricequotation/tests/main.py new file mode 100644 index 0000000000..51e5b87d45 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/main.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +import unittest + +from openprocurement.tender.pricequotation.tests import\ + award, bid, document, tender + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(award.suite()) + suite.addTest(bid.suite()) + suite.addTest(document.suite()) + suite.addTest(tender.suite()) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/tender.py b/src/openprocurement/tender/pricequotation/tests/tender.py new file mode 100644 index 0000000000..1baff78bd2 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/tender.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +import os +import unittest + +from openprocurement.api.tests.base import snitch +from openprocurement.tender.pricequotation.tests.base import ( + BaseTenderWebTest, + TenderContentWebTest, + test_tender_data, + BaseApiWebTest, +) + +from openprocurement.tender.pricequotation.tests.tender_blanks import ( + simple_add_tender, + + listing, + listing_draft, + listing_changes, + + one_valid_bid_tender, + one_invalid_bid_tender, + first_bid_tender, + + create_tender, + create_tender_draft, + create_tender_generated, + create_tender_invalid, + create_tender_with_inn, + + invalid_tender_conditions, + patch_tender, + patch_tender_by_pq_bot, + tender_owner_can_change_in_draft, + tender_owner_cannot_change_in_draft, + required_field_deletion, + tender_Administrator_change, + tender_fields, + lost_contract_for_active_award, + create_tender_in_not_draft_status, +) +from openprocurement.tender.belowthreshold.tests.tender_blanks import ( + guarantee, + create_tender_with_inn_before, + tender_milestones_required, + coordinates_reg_exp, + get_tender, + tender_not_found, + dateModified_tender, + patch_not_author, + tender_items_float_quantity, + patch_tender_jsonpatch, + tender_funders, + tender_with_main_procurement_category, + create_tender_with_inn_before, + tender_token_invalid, +) + +class TenderResourceTestMixin(object): + test_listing_changes = snitch(listing_changes) + test_listing_draft = snitch(listing_draft) + test_listing = snitch(listing) + test_create_tender_draft = snitch(create_tender_draft) + test_tender_owner_can_change_in_draft = snitch(tender_owner_can_change_in_draft) + test_tender_owner_cannot_change_in_draft = snitch(tender_owner_cannot_change_in_draft) + test_create_tender = snitch(create_tender) + test_get_tender = snitch(get_tender) + test_dateModified_tender = snitch(dateModified_tender) + test_tender_not_found = snitch(tender_not_found) + test_tender_Administrator_change = snitch(tender_Administrator_change) + test_patch_not_author = snitch(patch_not_author) + test_tender_funders = snitch(tender_funders) + test_tender_with_main_procurement_category = snitch(tender_with_main_procurement_category) + test_tender_token_invalid = snitch(tender_token_invalid) + test_create_tender_in_not_draft_status = snitch(create_tender_in_not_draft_status) + + +class TenderTest(BaseApiWebTest): + initial_data = test_tender_data + + test_simple_add_tender = snitch(simple_add_tender) + + +class TestCoordinatesRegExp(unittest.TestCase): + + test_coordinates_reg_exp = snitch(coordinates_reg_exp) + + +class TenderResourceTest(BaseTenderWebTest, TenderResourceTestMixin): + initial_data = test_tender_data + initial_auth = ("Basic", ("broker", "")) + + Test_guarantee = snitch(guarantee) + test_create_tender_invalid = snitch(create_tender_invalid) + test_create_tender_generated = snitch(create_tender_generated) + test_tender_fields = snitch(tender_fields) + test_tender_items_float_quantity = snitch(tender_items_float_quantity) + test_patch_tender_jsonpatch = snitch(patch_tender_jsonpatch) + test_patch_tender = snitch(patch_tender) + test_required_field_deletion = snitch(required_field_deletion) + test_create_tender_with_inn = snitch(create_tender_with_inn) + test_create_tender_with_inn_before = snitch(create_tender_with_inn_before) + test_patch_tender_by_pq_bot = snitch(patch_tender_by_pq_bot) + test_invalid_tender_conditions = snitch(invalid_tender_conditions) + + +class TenderProcessTest(TenderContentWebTest): + initial_auth = ("Basic", ("broker", "")) + initial_data = test_tender_data + initial_status = 'active.tendering' + need_tender = True + + test_one_valid_bid_tender = snitch(one_valid_bid_tender) + test_one_invalid_bid_tender = snitch(one_invalid_bid_tender) + test_first_bid_tender = snitch(first_bid_tender) + test_lost_contract_for_active_award = snitch(lost_contract_for_active_award) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TenderProcessTest)) + suite.addTest(unittest.makeSuite(TenderResourceTest)) + suite.addTest(unittest.makeSuite(TenderTest)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/src/openprocurement/tender/pricequotation/tests/tender_blanks.py b/src/openprocurement/tender/pricequotation/tests/tender_blanks.py new file mode 100644 index 0000000000..dc18887949 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/tender_blanks.py @@ -0,0 +1,1665 @@ +# -*- coding: utf-8 -*- +import mock +from uuid import uuid4 +from copy import deepcopy +from datetime import timedelta +from iso8601 import parse_date + +from openprocurement.api.utils import get_now +from openprocurement.api import validation +from openprocurement.api.constants import ( + COORDINATES_REG_EXP, + ROUTE_PREFIX, + CPV_BLOCK_FROM, + NOT_REQUIRED_ADDITIONAL_CLASSIFICATION_FROM, + RELEASE_2020_04_19, + SANDBOX_MODE, +) +from openprocurement.tender.core.constants import CPV_ITEMS_CLASS_FROM +from openprocurement.tender.core.tests.cancellation import activate_cancellation_after_2020_04_19 +from openprocurement.tender.pricequotation.models import PriceQuotationTender as Tender +from openprocurement.tender.belowthreshold.utils import calculate_tender_business_date +from openprocurement.tender.pricequotation.tests.base import ( + test_organization, + test_author, + test_cancellation, + test_shortlisted_firms, + test_short_profile, + test_requirement_response_valid, +) +from openprocurement.tender.pricequotation.tests.data import test_milestones +# TenderTest +from openprocurement.tender.core.tests.base import change_auth +from openprocurement.tender.pricequotation.constants import PMT, PQ_KINDS + + +def simple_add_tender(self): + + u = Tender(self.initial_data) + u.tenderID = "UA-X" + + assert u.id is None + assert u.rev is None + + u.store(self.db) + + assert u.id is not None + assert u.rev is not None + + fromdb = self.db.get(u.id) + + assert u.tenderID == fromdb["tenderID"] + assert u.doc_type == "Tender" + + u.delete_instance(self.db) + + +def listing(self): + response = self.app.get("/tenders") + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 0) + + tenders = [] + + for i in range(3): + offset = get_now().isoformat() + response = self.app.post_json("/tenders", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.tender_id = response.json['data']['id'] + self.set_status('active.tendering') + tender = self.app.get("/tenders/{}".format(self.tender_id)).json['data'] + tenders.append(tender) + + ids = ",".join([i["id"] for i in tenders]) + + while True: + response = self.app.get("/tenders") + self.assertTrue(ids.startswith(",".join([i["id"] for i in response.json["data"]]))) + if len(response.json["data"]) == 3: + break + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified"])) + self.assertEqual(set([i["id"] for i in response.json["data"]]), set([i["id"] for i in tenders])) + self.assertEqual(set([i["dateModified"] for i in response.json["data"]]), set([i["dateModified"] for i in tenders])) + self.assertEqual([i["dateModified"] for i in response.json["data"]], sorted([i["dateModified"] for i in tenders])) + + response = self.app.get("/tenders?limit=2") + self.assertEqual(response.status, "200 OK") + self.assertNotIn("prev_page", response.json) + self.assertEqual(len(response.json["data"]), 2) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 1) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 0) + + response = self.app.get("/tenders", params=[("opt_fields", "status")]) + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified", u"status"])) + self.assertIn("opt_fields=status", response.json["next_page"]["uri"]) + + response = self.app.get("/tenders", params=[("opt_fields", "status")]) + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified", u"status"])) + self.assertIn("opt_fields=status", response.json["next_page"]["uri"]) + + response = self.app.get("/tenders?descending=1") + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified"])) + self.assertEqual(set([i["id"] for i in response.json["data"]]), set([i["id"] for i in tenders])) + self.assertEqual( + [i["dateModified"] for i in response.json["data"]], sorted([i["dateModified"] for i in tenders], reverse=True) + ) + + response = self.app.get("/tenders?descending=1&limit=2") + self.assertEqual(response.status, "200 OK") + self.assertNotIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 2) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertNotIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 1) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertNotIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 0) + + test_tender_data2 = self.initial_data.copy() + test_tender_data2["mode"] = "test" + response = self.app.post_json("/tenders", {"data": test_tender_data2}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + +def listing_changes(self): + response = self.app.get("/tenders?feed=changes") + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 0) + + tenders = [] + + for i in range(3): + response = self.app.post_json("/tenders", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.tender_id = response.json['data']['id'] + self.set_status('active.tendering') + tender = self.app.get("/tenders/{}".format(self.tender_id)).json['data'] + tenders.append(tender) + ids = ",".join([i["id"] for i in tenders]) + + while True: + response = self.app.get("/tenders?feed=changes") + self.assertTrue(ids.startswith(",".join([i["id"] for i in response.json["data"]]))) + if len(response.json["data"]) == 3: + break + + self.assertEqual(",".join([i["id"] for i in response.json["data"]]), ids) + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified"])) + self.assertEqual(set([i["id"] for i in response.json["data"]]), set([i["id"] for i in tenders])) + self.assertEqual(set([i["dateModified"] for i in response.json["data"]]), set([i["dateModified"] for i in tenders])) + self.assertEqual([i["dateModified"] for i in response.json["data"]], sorted([i["dateModified"] for i in tenders])) + + response = self.app.get("/tenders?feed=changes&limit=2") + self.assertEqual(response.status, "200 OK") + self.assertNotIn("prev_page", response.json) + self.assertEqual(len(response.json["data"]), 2) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 1) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 0) + + response = self.app.get("/tenders?feed=changes", params=[("opt_fields", "status")]) + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified", u"status"])) + self.assertIn("opt_fields=status", response.json["next_page"]["uri"]) + + response = self.app.get("/tenders?feed=changes", params=[("opt_fields", "status")]) + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified", u"status"])) + self.assertIn("opt_fields=status", response.json["next_page"]["uri"]) + + response = self.app.get("/tenders?feed=changes&descending=1") + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified"])) + self.assertEqual(set([i["id"] for i in response.json["data"]]), set([i["id"] for i in tenders])) + self.assertEqual( + [i["dateModified"] for i in response.json["data"]], sorted([i["dateModified"] for i in tenders], reverse=True) + ) + + response = self.app.get("/tenders?feed=changes&descending=1&limit=2") + self.assertEqual(response.status, "200 OK") + self.assertNotIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 2) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertNotIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 1) + + response = self.app.get(response.json["next_page"]["path"].replace(ROUTE_PREFIX, "")) + self.assertEqual(response.status, "200 OK") + self.assertNotIn("descending=1", response.json["prev_page"]["uri"]) + self.assertEqual(len(response.json["data"]), 0) + + test_tender_data2 = self.initial_data.copy() + test_tender_data2["mode"] = "test" + response = self.app.post_json("/tenders", {"data": test_tender_data2}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + +def listing_draft(self): + response = self.app.get("/tenders") + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 0) + + tenders = [] + data = self.initial_data.copy() + data.update({"status": "draft"}) + for i in range(3): + response = self.app.post_json("/tenders", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.tender_id = response.json['data']['id'] + self.set_status('active.tendering') + tender = self.app.get("/tenders/{}".format(self.tender_id)).json['data'] + tenders.append(tender) + + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + ids = ",".join([i["id"] for i in tenders]) + + while True: + response = self.app.get("/tenders") + self.assertTrue(ids.startswith(",".join([i["id"] for i in response.json["data"]]))) + if len(response.json["data"]) == 3: + break + + self.assertEqual(len(response.json["data"]), 3) + self.assertEqual(set(response.json["data"][0]), set([u"id", u"dateModified"])) + self.assertEqual(set([i["id"] for i in response.json["data"]]), set([i["id"] for i in tenders])) + self.assertEqual(set([i["dateModified"] for i in response.json["data"]]), set([i["dateModified"] for i in tenders])) + self.assertEqual([i["dateModified"] for i in response.json["data"]], sorted([i["dateModified"] for i in tenders])) + + +def create_tender_invalid(self): + request_path = "/tenders" + response = self.app.post(request_path, "data", status=415) + self.assertEqual(response.status, "415 Unsupported Media Type") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": u"Content-Type header should be one of ['application/json']", + u"location": u"header", + u"name": u"Content-Type", + } + ], + ) + + response = self.app.post(request_path, "data", content_type="application/json", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": u"No JSON object could be decoded", u"location": u"body", u"name": u"data"}], + ) + + response = self.app.post_json(request_path, "data", status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"not_data": {}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"data": []}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Data not available", u"location": u"body", u"name": u"data"}] + ) + + response = self.app.post_json(request_path, {"data": {"procurementMethodType": "invalid_value"}}, status=415) + self.assertEqual(response.status, "415 Unsupported Media Type") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": u"Not implemented", u"location": u"data", u"name": u"procurementMethodType"}], + ) + + response = self.app.post_json(request_path, {"data": {"invalid_field": "invalid_value", "procurementMethodType": PMT}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Rogue field", u"location": u"body", u"name": u"invalid_field"}] + ) + + response = self.app.post_json(request_path, {"data": {"value": "invalid_value", "procurementMethodType": PMT}}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [u"Please use a mapping for this field or Value instance instead of unicode."], + u"location": u"body", + u"name": u"value", + } + ], + ) + + response = self.app.post_json(request_path, {"data": {"procurementMethod": "invalid_value", "procurementMethodType": PMT }}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + + self.assertIn( + { + u"description": [u"Value must be one of ['selective']."], + u"location": u"body", + u"name": u"procurementMethod", + }, + response.json["errors"], + ) + + self.assertIn( + {u"description": [u"This field is required."], u"location": u"body", u"name": u"tenderPeriod"}, + response.json["errors"], + ) + + self.assertIn( + {u"description": [u"This field is required."], u"location": u"body", u"name": u"items"}, response.json["errors"] + ) + + data = self.initial_data["tenderPeriod"] + self.initial_data["tenderPeriod"] = {"startDate": "2014-10-31T00:00:00", "endDate": "2014-10-01T00:00:00"} + response = self.app.post_json(request_path, {"data": self.initial_data}, status=422) + self.initial_data["tenderPeriod"] = data + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": {u"startDate": [u"period should begin before its end"]}, + u"location": u"body", + u"name": u"tenderPeriod", + } + ], + ) + + now = get_now() + self.initial_data["awardPeriod"] = {"startDate": now.isoformat(), "endDate": now.isoformat()} + response = self.app.post_json(request_path, {"data": self.initial_data}, status=422) + del self.initial_data["awardPeriod"] + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [{u"description": [u"period should begin after tenderPeriod"], u"location": u"body", u"name": u"awardPeriod"}], + ) + + data = self.initial_data["items"][0].pop("additionalClassifications") + if get_now() > CPV_ITEMS_CLASS_FROM: + cpv_code = self.initial_data["items"][0]["classification"]["id"] + self.initial_data["items"][0]["classification"]["id"] = "99999999-9" + + status = 422 if get_now() < NOT_REQUIRED_ADDITIONAL_CLASSIFICATION_FROM else 201 + response = self.app.post_json(request_path, {"data": self.initial_data}, status=status) + self.initial_data["items"][0]["additionalClassifications"] = data + if get_now() > CPV_ITEMS_CLASS_FROM: + self.initial_data["items"][0]["classification"]["id"] = cpv_code + if status == 201: + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.status, "201 Created") + else: + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": [{u"additionalClassifications": [u"This field is required."]}], + u"location": u"body", + u"name": u"items", + } + ], + ) + + data = test_organization["contactPoint"]["telephone"] + del test_organization["contactPoint"]["telephone"] + response = self.app.post_json(request_path, {"data": self.initial_data}, status=422) + test_organization["contactPoint"]["telephone"] = data + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": {u"contactPoint": {u"email": [u"telephone or email should be present"]}}, + u"location": u"body", + u"name": u"procuringEntity", + } + ], + ) + + cpv = self.initial_data["items"][0]["classification"]["id"] + self.initial_data["items"][0]["classification"]["id"] = u"160173000-1" + response = self.app.post_json(request_path, {"data": self.initial_data}, status=422) + self.initial_data["items"][0]["classification"]["id"] = cpv + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertIn(u"classification", response.json["errors"][0][u"description"][0]) + self.assertIn(u"id", response.json["errors"][0][u"description"][0][u"classification"]) + self.assertIn("Value must be one of [u", response.json["errors"][0][u"description"][0][u"classification"][u"id"][0]) + + cpv = self.initial_data["items"][0]["classification"]["id"] + if get_now() < CPV_BLOCK_FROM: + self.initial_data["items"][0]["classification"]["scheme"] = u"CPV" + self.initial_data["items"][0]["classification"]["id"] = u"00000000-0" + response = self.app.post_json(request_path, {"data": self.initial_data}, status=422) + if get_now() < CPV_BLOCK_FROM: + self.initial_data["items"][0]["classification"]["scheme"] = u"CPV" + self.initial_data["items"][0]["classification"]["id"] = cpv + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertIn(u"classification", response.json["errors"][0][u"description"][0]) + self.assertIn(u"id", response.json["errors"][0][u"description"][0][u"classification"]) + self.assertIn("Value must be one of [u", response.json["errors"][0][u"description"][0][u"classification"][u"id"][0]) + + procuringEntity = self.initial_data["procuringEntity"] + data = self.initial_data["procuringEntity"].copy() + del data["kind"] + self.initial_data["procuringEntity"] = data + response = self.app.post_json(request_path, {"data": self.initial_data}, status=403) + self.initial_data["procuringEntity"] = procuringEntity + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": u"'' procuringEntity cannot publish this type of procedure. " + u"Only general, special, defense, other, social, authority are allowed.", + u"location": u"procuringEntity", + u"name": u"kind", + } + ], + ) + + data = deepcopy(self.initial_data) + data['milestones'] = test_milestones + response = self.app.post_json(request_path, {"data": data}, status=422) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{ + u"description": [u"Milestones are not applicable to pricequotation"], + u"location": u"body", + u"name": u"milestones" + }], + ) + + data = deepcopy(self.initial_data) + data["procuringEntity"]['kind'] = 'central' + response = self.app.post_json(request_path, {"data": data}, status=403) + self.assertEqual(response.status, '403 Forbidden') + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{ + u"description": "u'central' procuringEntity cannot publish this type of procedure. Only general, special, defense, other, social, authority are allowed.", + u"location": u"procuringEntity", + u"name": u"kind" + }], + ) + + +def create_tender_with_inn(self): + request_path = "/tenders" + + addit_classif = [ + {"scheme": "INN", "id": "17.21.1", "description": "папір і картон гофровані, паперова й картонна тара"} + ] + data = self.initial_data["items"][0]["classification"]["id"] + self.initial_data["items"][0]["classification"]["id"] = u"33611000-6" + orig_addit_classif = self.initial_data["items"][0]["additionalClassifications"] + self.initial_data["items"][0]["additionalClassifications"] = addit_classif + response = self.app.post_json(request_path, {"data": self.initial_data}) + self.initial_data["items"][0]["additionalClassifications"] = orig_addit_classif + self.initial_data["items"][0]["classification"]["id"] = data + self.assertEqual(response.status, "201 Created") + + addit_classif = [ + {"scheme": "NotINN", "id": "17.21.1", "description": "папір і картон гофровані, паперова й картонна тара"}, + {"scheme": "NotINN", "id": "17.21.1", "description": "папір і картон гофровані, паперова й картонна тара"}, + ] + data = self.initial_data["items"][0]["classification"]["id"] + self.initial_data["items"][0]["classification"]["id"] = u"33652000-5" + orig_addit_classif = self.initial_data["items"][0]["additionalClassifications"] + self.initial_data["items"][0]["additionalClassifications"] = addit_classif + response = self.app.post_json(request_path, {"data": self.initial_data}) + self.initial_data["items"][0]["additionalClassifications"] = orig_addit_classif + self.initial_data["items"][0]["classification"]["id"] = data + self.assertEqual(response.status, "201 Created") + + +def create_tender_generated(self): + data = self.initial_data.copy() + data.update({"id": "hash", "doc_id": "hash2", "tenderID": "hash3"}) + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + if "procurementMethodDetails" in tender: + tender.pop("procurementMethodDetails") + self.assertEqual( + set(tender), + set( + [ + u"procurementMethodType", + u"id", + u"date", + u"dateModified", + u"tenderID", + u"status", + u"tenderPeriod", + u"items", + u"procuringEntity", + u"procurementMethod", + u"awardCriteria", + u"submissionMethod", + u"title", + u"owner", + u"mainProcurementCategory", + u"profile", + u"value" + ] + ), + ) + self.assertNotEqual(data["id"], tender["id"]) + self.assertNotEqual(data["doc_id"], tender["id"]) + self.assertNotEqual(data["tenderID"], tender["tenderID"]) + + +def create_tender_draft(self): + data = self.initial_data.copy() + data.update({"status": "draft"}) + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + token = response.json["access"]["token"] + self.assertEqual(tender["status"], "draft") + self.assertNotIn("noticePublicationDate", tender) + self.assertNotIn("unsuccessfulReason", tender) + + if SANDBOX_MODE: + period = { + 'endDate': (get_now() + timedelta(minutes=1)).isoformat() + } + else: + period = { + 'endDate': (get_now() + timedelta(days=1)).isoformat() + } + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), + {"data": {"status": self.primary_tender_status, "tenderPeriod": period}}, + status=422 + ) + + self.assertEqual( + response.json["errors"], + [{u'description': [u'the tenderPeriod cannot end earlier than 2 business days after the start'], + u'location': u'body', + u'name': u'tenderPeriod'}] + ) + + forbidden_statuses = ("draft.unsuccessful", "active.tendering", "active.qualification", "active.awarded", + "complete", "cancelled", "unsuccessful") + current_status = tender["status"] + for forbidden_status in forbidden_statuses: + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), + {"data": {"status": forbidden_status}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json['status'], "error") + self.assertEqual( + response.json['errors'], + [{u'description': u"tender_owner can't switch tender from status ({}) to ({})".format(current_status, + forbidden_status), + u'location': u'body', + u'name': u'data'}] + ) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), + {"data": {"procuringEntity": {"kind": 'central'}}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json['status'], 'error') + self.assertEqual( + response.json['errors'], + [{ + u'description': u"u'central' procuringEntity cannot publish this type of procedure. Only general, special, defense, other, social, authority are allowed.", + u'location': u'procuringEntity', + u'name': u'kind' + }] + ) + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), + {"data": {"status": self.primary_tender_status, "unsuccessfulReason": ["some value from buyer"]}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + self.assertEqual(tender["status"], self.primary_tender_status) + self.assertEqual(tender["noticePublicationDate"], tender["tenderPeriod"]["startDate"]) + self.assertNotIn("unsuccessfulReason", tender) + + response = self.app.get("/tenders/{}".format(tender["id"])) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + self.assertEqual(tender["status"], self.primary_tender_status) + + +def create_tender_in_not_draft_status(self): + data = self.initial_data.copy() + forbidden_statuses = ("draft.unsuccessful", "active.tendering", "active.qualification", "active.awarded", + "complete", "cancelled", "unsuccessful") + for forbidden_status in forbidden_statuses: + data.update({"status": forbidden_status}) + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + token = response.json["access"]["token"] + self.assertEqual(tender["status"], "draft") + + +def tender_owner_can_change_in_draft(self): + data = self.initial_data.copy() + data.update({"status": "draft"}) + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + token = response.json["access"]["token"] + self.assertEqual(tender["status"], "draft") + + general = { + "numberOfBidders": 1, + "tenderPeriod": {"endDate": (get_now() + timedelta(days=14)).isoformat()}, + "procuringEntity": {"name": u"Національне управління справами"}, + "mainProcurementCategory": u"services", + "guarantee": {"amount": 50}, + } + descriptions = { + "description": u"Some text 1", + "description_en": u"Some text 2", + "description_ru": u"Some text 3", + "procurementMethodRationale": u"Some text 4", + "procurementMethodRationale_en": u"Some text 5", + "procurementMethodRationale_ru": u"Some text 6", + "submissionMethodDetails": u"Some text 7", + "submissionMethodDetails_en": u"Some text 8", + "submissionMethodDetails_ru": u"Some text 9" + } + titles = { + "title": u"Test title 1", + "title_en": u"Test title 2", + "title_ru": u"Test title 3" + } + criterias = { + "eligibilityCriteria": u"Test criteria 1", + "eligibilityCriteria_en": u"Test criteria 2", + "eligibilityCriteria_ru": u"Test criteria 3", + "awardCriteriaDetails": u"Test criteria 4", + "awardCriteriaDetails_en": u"Test criteria 5", + "awardCriteriaDetails_ru": u"Test criteria 6" + } + lists = { + "buyers": [ + { + "name": u"John Doe", + "identifier": { + "scheme": u"AE-DCCI", + "id": u"AE1" + } + } + ], + "funders": [ + { + "name": u"First funder", + "identifier": { + "scheme": u"XM-DAC", + "id": u"44000" + }, + "address": { + "countryName": u"Японія" + }, + "contactPoint": { + "name": u"Funder name", + "email": u"fake_japan_email@gmail.net" + } + } + ], + "items": [ + { + "description": u"New description" + } + ] + } + status = { + "status": "draft.publishing" + } + + # general + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": general} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender["numberOfBidders"], general["numberOfBidders"]) + self.assertNotEqual(tender["numberOfBidders"], data.get("numberOfBidders")) + self.assertEqual(tender["mainProcurementCategory"], general["mainProcurementCategory"]) + self.assertNotEqual(tender["mainProcurementCategory"], data.get("mainProcurementCategory")) + self.assertEqual(tender["tenderPeriod"]["endDate"], general["tenderPeriod"]["endDate"]) + self.assertNotEqual(tender["tenderPeriod"]["endDate"], data.get("tenderPeriod", {}).get("endDate")) + self.assertEqual(tender["procuringEntity"]["name"], general["procuringEntity"]["name"]) + self.assertNotEqual(tender["procuringEntity"]["name"], data.get("procuringEntity", {}).get("name")) + self.assertEqual(tender["guarantee"]["amount"], general["guarantee"]["amount"]) + self.assertNotEqual(tender["guarantee"]["amount"], data.get("guarantee", {}).get("amount")) + + # descriptions + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": descriptions} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender["description"], descriptions["description"]) + self.assertNotEqual(tender["description"], data.get("description")) + self.assertEqual(tender["description_en"], descriptions["description_en"]) + self.assertNotEqual(tender["description_en"], data.get("description_en")) + self.assertEqual(tender["description_ru"], descriptions["description_ru"]) + self.assertNotEqual(tender["description_ru"], data.get("description_ru")) + self.assertEqual(tender["procurementMethodRationale"], descriptions["procurementMethodRationale"]) + self.assertNotEqual(tender["procurementMethodRationale"], data.get("procurementMethodRationale")) + self.assertEqual(tender["procurementMethodRationale_en"], descriptions["procurementMethodRationale_en"]) + self.assertNotEqual(tender["procurementMethodRationale_en"], data.get("procurementMethodRationale_en")) + self.assertEqual(tender["procurementMethodRationale_ru"], descriptions["procurementMethodRationale_ru"]) + self.assertNotEqual(tender["procurementMethodRationale_ru"], data.get("procurementMethodRationale_ru")) + self.assertEqual(tender["submissionMethodDetails"], descriptions["submissionMethodDetails"]) + self.assertNotEqual(tender["submissionMethodDetails"], data.get("submissionMethodDetails")) + self.assertEqual(tender["submissionMethodDetails_en"], descriptions["submissionMethodDetails_en"]) + self.assertNotEqual(tender["submissionMethodDetails_en"], data.get("submissionMethodDetails_en")) + self.assertEqual(tender["submissionMethodDetails_ru"], descriptions["submissionMethodDetails_ru"]) + self.assertNotEqual(tender["submissionMethodDetails_ru"], data.get("submissionMethodDetails_ru")) + + # titles + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": titles} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender["title"], titles["title"]) + self.assertNotEqual(tender["title"], data.get("title")) + self.assertEqual(tender["title_en"], titles["title_en"]) + self.assertNotEqual(tender["title_en"], data.get("title_en")) + self.assertEqual(tender["title_ru"], titles["title_ru"]) + self.assertNotEqual(tender["title_ru"], data.get("title_ru")) + + # criterias + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": criterias} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender["eligibilityCriteria"], criterias["eligibilityCriteria"]) + self.assertNotEqual(tender["eligibilityCriteria"], data.get("eligibilityCriteria")) + self.assertEqual(tender["eligibilityCriteria_en"], criterias["eligibilityCriteria_en"]) + self.assertNotEqual(tender["eligibilityCriteria_en"], data.get("eligibilityCriteria_en")) + self.assertEqual(tender["eligibilityCriteria_ru"], criterias["eligibilityCriteria_ru"]) + self.assertNotEqual(tender["eligibilityCriteria_ru"], data.get("eligibilityCriteria_ru")) + self.assertEqual(tender["awardCriteriaDetails"], criterias["awardCriteriaDetails"]) + self.assertNotEqual(tender["awardCriteriaDetails"], data.get("awardCriteriaDetails")) + self.assertEqual(tender["awardCriteriaDetails_en"], criterias["awardCriteriaDetails_en"]) + self.assertNotEqual(tender["awardCriteriaDetails_en"], data.get("awardCriteriaDetails_en")) + self.assertEqual(tender["awardCriteriaDetails_ru"], criterias["awardCriteriaDetails_ru"]) + self.assertNotEqual(tender["awardCriteriaDetails_ru"], data.get("awardCriteriaDetails_ru")) + + # lists + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": lists} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender["funders"], lists["funders"]) + self.assertEqual(tender["buyers"], lists["buyers"]) + + self.assertEqual(tender["items"][0]["description"], lists["items"][0]["description"]) + + # status + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": status} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender["status"], status["status"]) + self.assertNotEqual(tender["status"], data["status"]) + + +def tender_owner_cannot_change_in_draft(self): + data = self.initial_data.copy() + data.update({"status": "draft"}) + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + token = response.json["access"]["token"] + self.assertEqual(tender["status"], "draft") + + general = { + "tenderID": u"Some id", + "procurementMethodType": u"belowThreshold", + "procurementMethod": u"selective", + "submissionMethod": u"written", + "awardCriteria": u"bestProposal", + "mode": u"test" + } + owner = { + "owner": u"Test owner", + "transfer_token": u"17bc682ec79245bca7d9cdbabbfce8f8", + "owner_token": u"17bc682ec79245bca7d9cdbabbfce8f7" + } + time = { + "awardPeriod": {"endDate": (get_now() + timedelta(days=14)).isoformat()}, + "date": (get_now() + timedelta(days=1)).isoformat(), + "dateModified": (get_now() + timedelta(days=1)).isoformat(), + } + lists = { + "revisions": [{"author": "Some author"}], + "plans": [{"id": uuid4().hex}], + "cancellations": [ + { + "reason": u"Some reason", + "reasonType": u"noDemand" + } + ], + } + + # general + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": general} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertNotEqual(tender.get("tenderID"), general["tenderID"]) + self.assertNotEqual(tender.get("procurementMethodType"), general["procurementMethodType"]) + self.assertEqual(tender.get("procurementMethod"), general["procurementMethod"]) + self.assertNotEqual(tender.get("submissionMethod"), general["submissionMethod"]) + self.assertNotEqual(tender.get("awardCriteria"), general["awardCriteria"]) + self.assertNotEqual(tender.get("mode"), general["mode"]) + + # owner + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": owner} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertNotEqual(tender.get("owner"), owner["owner"]) + self.assertNotEqual(tender.get("transfer_token"), owner["transfer_token"]) + self.assertNotEqual(tender.get("owner_token"), owner["owner_token"]) + + # time + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": time} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertNotEqual(tender.get("awardPeriod", {}).get("endDate"), time["awardPeriod"]["endDate"]) + self.assertNotEqual(tender.get("date"), time["date"]) + self.assertNotEqual(tender.get("dateModified"), time["dateModified"]) + + # lists + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": lists} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + self.assertEqual(tender.get("revisions", []), []) + self.assertEqual(tender.get("plans", []), []) + self.assertEqual(tender.get("cancellations", []), []) + + +def create_tender(self): + response = self.app.get("/tenders") + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 0) + + response = self.app.post_json("/tenders", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + + response = self.app.get("/tenders/{}".format(tender["id"])) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(set(response.json["data"]), set(tender)) + self.assertEqual(response.json["data"], tender) + + response = self.app.post_json("/tenders?opt_jsonp=callback", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/javascript") + self.assertIn('callback({"', response.body) + + response = self.app.post_json("/tenders?opt_pretty=1", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.assertIn('{\n "', response.body) + + response = self.app.post_json("/tenders", {"data": self.initial_data, "options": {"pretty": True}}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.assertIn('{\n "', response.body) + + tender_data = deepcopy(self.initial_data) + tender_data["guarantee"] = {"amount": 100500, "currency": "USD"} + response = self.app.post_json("/tenders", {"data": tender_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + data = response.json["data"] + self.assertIn("guarantee", data) + self.assertEqual(data["guarantee"]["amount"], 100500) + self.assertEqual(data["guarantee"]["currency"], "USD") + + data = deepcopy(self.initial_data) + del data["items"][0]["deliveryAddress"]["postalCode"] + del data["items"][0]["deliveryAddress"]["locality"] + del data["items"][0]["deliveryAddress"]["streetAddress"] + del data["items"][0]["deliveryAddress"]["region"] + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.assertNotIn("postalCode", response.json["data"]["items"][0]["deliveryAddress"]) + self.assertNotIn("locality", response.json["data"]["items"][0]["deliveryAddress"]) + self.assertNotIn("streetAddress", response.json["data"]["items"][0]["deliveryAddress"]) + self.assertNotIn("region", response.json["data"]["items"][0]["deliveryAddress"]) + + for kind in PQ_KINDS: + data = deepcopy(self.initial_data) + data['procuringEntity']['kind'] = kind + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json['data']['procuringEntity']['kind'], + kind + ) + + +def tender_fields(self): + response = self.app.post_json("/tenders", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + tender = response.json["data"] + self.assertEqual( + set(tender) - set(self.initial_data), + set( + [ + u"id", + u"dateModified", + u"tenderID", + u"date", + u"status", + u"awardCriteria", + u"submissionMethod", + u"owner", + ] + ), + ) + self.assertIn(tender["id"], response.headers["Location"]) + + +def patch_tender(self): + data = self.initial_data.copy() + data["procuringEntity"]["contactPoint"]["faxNumber"] = u"0440000000" + response = self.app.get("/tenders") + self.assertEqual(response.status, "200 OK") + self.assertEqual(len(response.json["data"]), 0) + + response = self.app.post_json("/tenders", {"data": data}) + self.assertEqual(response.status, "201 Created") + tender = response.json["data"] + owner_token = response.json["access"]["token"] + dateModified = tender.pop("dateModified") + + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), + {"data": {"milestones": test_milestones}}, + status=422 + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{ + u"description": [u"Milestones are not applicable to pricequotation"], + u"location": u"body", + u"name": u"milestones" + }], + ) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"procuringEntity": {"kind": "defense"}}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["procuringEntity"]["kind"], "defense") + tender[u"procuringEntity"]['kind'] = u"defense" + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), + {"data": {"procuringEntity": {"contactPoint": {"faxNumber": None}}}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertNotIn("faxNumber", response.json["data"]["procuringEntity"]["contactPoint"]) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), + {"data": {"procuringEntity": {"contactPoint": {"faxNumber": u"0440000000"}}}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertIn("startDate", response.json["data"]["tenderPeriod"]) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"procurementMethodRationale": "Open"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + new_tender = response.json["data"] + new_dateModified = new_tender.pop("dateModified") + tender[u"procurementMethodRationale"] = u"Open" + self.assertEqual(tender, new_tender) + self.assertNotEqual(dateModified, new_dateModified) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"dateModified": new_dateModified}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + new_tender2 = response.json["data"] + new_dateModified2 = new_tender2.pop("dateModified") + self.assertEqual(new_tender, new_tender2) + self.assertEqual(new_dateModified, new_dateModified2) + + revisions = self.db.get(tender["id"]).get("revisions") + self.assertEqual(revisions[-1][u"changes"][0]["op"], u"remove") + self.assertEqual(revisions[-1][u"changes"][0]["path"], u"/procurementMethodRationale") + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"items": [data["items"][0]]}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"items": [{}, data["items"][0]]}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + item0 = response.json["data"]["items"][0] + item1 = response.json["data"]["items"][1] + self.assertNotEqual(item0.pop("id"), item1.pop("id")) + self.assertEqual(item0, item1) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"items": [{}]}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(len(response.json["data"]["items"]), 1) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), + { + "data": { + "items": [ + { + "classification": { + "scheme": "ДК021", + "id": "55523100-3", + "description": "Послуги з харчування у школах", + } + } + ] + } + }, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), + {"data": {"guarantee": {"amount": 12, "valueAddedTaxIncluded": True}}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual( + response.json["errors"][0], + {u"description": {u"valueAddedTaxIncluded": u"Rogue field"}, u"location": u"body", u"name": u"guarantee"}, + ) + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"guarantee": {"amount": 12}}} + ) + self.assertEqual(response.status, "200 OK") + self.assertIn("guarantee", response.json["data"]) + self.assertEqual(response.json["data"]["guarantee"]["amount"], 12) + self.assertEqual(response.json["data"]["guarantee"]["currency"], "UAH") + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], owner_token), {"data": {"guarantee": {"currency": "USD"}}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["guarantee"]["currency"], "USD") + + # response = self.app.patch_json('/tenders/{}'.format(tender['id']), {'data': {'status': 'active.auction'}}) + # self.assertEqual(response.status, '200 OK') + + # response = self.app.get('/tenders/{}'.format(tender['id'])) + # self.assertEqual(response.status, '200 OK') + # self.assertEqual(response.content_type, 'application/json') + + tender_data = self.db.get(tender["id"]) + tender_data["status"] = "complete" + self.db.save(tender_data) + + +@mock.patch("openprocurement.tender.core.models.CANT_DELETE_PERIOD_START_DATE_FROM", get_now() - timedelta(days=1)) +def required_field_deletion(self): + response = self.app.post_json("/tenders", {"data": self.initial_data}) + self.assertEqual(response.status, "201 Created") + tender = response.json["data"] + token = response.json["access"]["token"] + + response = self.app.patch_json( + "/tenders/{}?acc_token={}".format(tender["id"], token), + {"data": {"tenderPeriod": {"startDate": None}}}, + status=422, + ) + self.assertEqual(response.status, "422 Unprocessable Entity") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], + [ + { + u"description": {u"startDate": [u"This field cannot be deleted"]}, + u"location": u"body", + u"name": u"tenderPeriod", + } + ], + ) + + +def tender_Administrator_change(self): + self.create_tender() + self.set_status('active.tendering') + cancellation = dict(**test_cancellation) + cancellation.update({ + "reasonType": "noDemand", + "status": "active", + }) + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), + {"data": cancellation}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + + self.app.authorization = ("Basic", ("administrator", "")) + response = self.app.patch_json("/tenders/{}".format(self.tender_id), {"data": {"mode": u"test"}}) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["data"]["mode"], u"test") + + +def patch_tender_by_pq_bot(self): + response = self.app.post_json("/tenders", {"data": deepcopy(self.initial_data)}) + self.assertEqual(response.status, "201 Created") + tender_id = response.json["data"]["id"] + owner_token = response.json["access"]["token"] + tender = response.json["data"] + + self.assertEqual(tender["status"], "draft") + self.assertEqual(len(tender["items"]), 1) + self.assertNotIn("shortlistedFirms", tender) + self.assertNotIn("classification", tender["items"][0]) + self.assertNotIn("unit", tender["items"][0]) + + data = {"data": { + "status": "draft.publishing", + "profile": test_short_profile["id"]} + } + response = self.app.patch_json("/tenders/{}?acc_token={}".format(tender_id, owner_token), data) + self.assertEqual(response.status, "200 OK") + tender = response.json["data"] + self.assertEqual(tender["status"], "draft.publishing") + self.assertEqual(tender["profile"], test_short_profile["id"]) + + items = deepcopy(tender["items"]) + items[0]["classification"] = test_short_profile["classification"] + items[0]["unit"] = test_short_profile["unit"] + amount = sum([item["quantity"] for item in items]) * test_short_profile["value"]["amount"] + value = deepcopy(test_short_profile["value"]) + value["amount"] = amount + criteria = deepcopy(test_short_profile["criteria"]) + data = { + "data": { + "status": "active.tendering", + "items": items, + "shortlistedFirms": test_shortlisted_firms, + "criteria": criteria, + "value": value + } + } + + # try to patch by user + for patch in ({'data': {'status': 'active.tendering'}}, data): + with change_auth(self.app, ("Basic", ("broker", ""))) as app: + resp = app.patch_json("/tenders/{}?acc_token={}".format(tender_id, owner_token), patch, status=403) + self.assertEqual(resp.status, "403 Forbidden") + self.assertEqual(resp.json['status'], "error") + self.assertEqual(resp.json['errors'], [ + {'description': "tender_owner can't switch tender from status (draft.publishing) to (active.tendering)", + 'location': 'body', + 'name': 'data'} + ]) + + # patch by bot + with change_auth(self.app, ("Basic", ("pricequotation", ""))) as app: + resp = app.patch_json("/tenders/{}".format(tender_id), data) + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.status, "200 OK") + tender = response.json["data"] + self.assertEqual(tender["status"], data["data"]["status"]) + self.assertIn("classification", tender["items"][0]) + self.assertIn("unit", tender["items"][0]) + self.assertEqual(len(tender["shortlistedFirms"]), len(test_shortlisted_firms)) + self.assertEqual(len(tender["criteria"]), len(test_short_profile["criteria"])) + self.assertEqual(tender["value"], value) + + # switch tender to `draft.unsuccessful` + response = self.app.post_json("/tenders", {"data": deepcopy(self.initial_data)}) + self.assertEqual(response.status, "201 Created") + tender_id = response.json["data"]["id"] + owner_token = response.json["access"]["token"] + tender = response.json["data"] + + self.assertEqual(tender["status"], "draft") + self.assertEqual(len(tender["items"]), 1) + self.assertNotIn("shortlistedFirms", tender) + self.assertNotIn("classification", tender["items"][0]) + self.assertNotIn("unit", tender["items"][0]) + + data = {"data": {"status": "draft.publishing", "profile": "a1b2c3-a1b2c3e4-f1g2i3-h1g2k3l4"}} + response = self.app.patch_json("/tenders/{}?acc_token={}".format(tender_id, owner_token), data, status=422) + self.assertEqual( + response.json["errors"], + [{"location": "body", "name": "profile", "description": ["The profile value doesn't match id pattern"]}] + ) + + # set not existed profile id + data["data"]["profile"] = "123456-12345678-123456-12345678" + response = self.app.patch_json("/tenders/{}?acc_token={}".format(tender_id, owner_token), data) + self.assertEqual(response.status, "200 OK") + tender = response.json["data"] + self.assertEqual(tender["status"], "draft.publishing") + self.assertEqual(tender["profile"], "123456-12345678-123456-12345678") + + with change_auth(self.app, ("Basic", ("pricequotation", ""))) as app: + self.app.patch_json( + "/tenders/{}".format(tender_id), + {"data": {"status": "draft.unsuccessful", "unsuccessfulReason": ["Profile not found in catalogue"]}} + ) + + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.status, "200 OK") + tender = response.json["data"] + self.assertEqual(tender["status"], "draft.unsuccessful") + self.assertEqual(tender["unsuccessfulReason"], ["Profile not found in catalogue"]) + self.assertNotIn("classification", tender["items"][0]) + self.assertNotIn("unit", tender["items"][0]) + self.assertNotIn("shortlistedFirms", tender) + +# TenderProcessTest + + +def invalid_tender_conditions(self): + # create tender + response = self.app.post_json("/tenders", {"data": self.initial_data}) + tender_id = self.tender_id = response.json["data"]["id"] + owner_token = response.json["access"]["token"] + # switch to active.tendering + self.set_status("active.tendering") + # cancellation + cancellation = dict(**test_cancellation) + cancellation.update({ + "reason": "invalid conditions", + "reasonType": "noDemand", + }) + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(tender_id, owner_token), + {"data": cancellation}, + ) + cancellation_id = response.json["data"]["id"] + response = self.app.post( + "/tenders/{}/cancellations/{}/documents?acc_token={}".format( + self.tender_id, cancellation_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content")], + ) + + response = self.app.patch_json( + "/tenders/{}/cancellations/{}?acc_token={}".format(tender_id, cancellation_id, owner_token), + {"data": {"status": "active"}}, + ) + + # check status + response = self.app.get("/tenders/{}".format(self.tender_id)) + self.assertEqual(response.json["data"]["status"], "cancelled") + + +def one_valid_bid_tender(self): + tender_id = self.tender_id + owner_token = self.tender_token + # create bid + self.app.authorization = ("Basic", ("broker", "")) + resp = self.app.post_json( + "/tenders/{}/bids".format(tender_id), {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid + }} + ) + token = resp.json['access']['token'] + # switch to active.qualification + self.set_status("active.qualification") + self.app.authorization = ("Basic", ("chronograph", "")) + response = self.app.patch_json("/tenders/{}".format(tender_id), {"data": {"id": tender_id}}) + # get awards + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}/awards?acc_token={}".format(tender_id, owner_token)) + # get pending award + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + award_date = [i["date"] for i in response.json["data"] if i["status"] == "pending"][0] + # set award as active + response = self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, token), + {"data": {"status": "active"}} + ) + self.assertNotEqual(response.json["data"]["date"], award_date) + + # get contract id + response = self.app.get("/tenders/{}".format(tender_id)) + contract_id = response.json["data"]["contracts"][-1]["id"] + # after stand slill period + self.set_status("active.awarded", 'end') + # sign contract + self.app.authorization = ("Basic", ("broker", "")) + self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(tender_id, contract_id, owner_token), + {"data": {"status": "active", "value": {"valueAddedTaxIncluded": False}}}, + ) + # check status + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.json["data"]["status"], "complete") + + +def one_invalid_bid_tender(self): + tender_id = self.tender_id + owner_token = self.tender_token + # create bid + self.app.authorization = ("Basic", ("broker", "")) + resp = self.app.post_json( + "/tenders/{}/bids".format(tender_id), {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}} + ) + token = resp.json['access']['token'] + # switch to active.qualification + self.set_status('active.tendering', 'end') + resp = self.check_chronograph() + self.assertEqual(resp.json['data']['status'], 'active.qualification') + # get awards + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}/awards?acc_token={}".format(tender_id, owner_token)) + # get pending award + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # set award as unsuccessful + self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, token), + {"data": {"status": "unsuccessful"}}, + ) + # check status + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.json["data"]["status"], "unsuccessful") + + +def first_bid_tender(self): + tender_id = self.tender_id + owner_token = self.tender_token + # create bid + response = self.app.post_json( + "/tenders/{}/bids".format(tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 450}, + "requirementResponses": test_requirement_response_valid + }} + ) + bid_1 = response.json["data"]["id"] + bid_token1 = response.json["access"]["token"] + + # create second bid + response = self.app.post_json( + "/tenders/{}/bids".format(tender_id), + {"data": { + "tenderers": [test_organization], + "value": {"amount": 300}, + "requirementResponses": test_requirement_response_valid + }} + ) + bid_2 = response.json["data"]["id"] + bid_token2 = response.json["access"]["token"] + self.set_status('active.tendering', 'end') + resp = self.check_chronograph() + self.assertEqual(resp.json['data']['status'], 'active.qualification') + # get awards + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}/awards?acc_token={}".format(tender_id, owner_token)) + # get pending award + award = [i for i in response.json["data"] if i["status"] == "pending"][0] + award_id = award['id'] + self.assertEqual(award['bid_id'], bid_2) + self.assertEqual(award['value']['amount'], 300) + # set award as unsuccessful + self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, bid_token2), + {"data": {"status": "unsuccessful"}}, + ) + # get awards + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}/awards?acc_token={}".format(tender_id, owner_token)) + # get pending award + award = [i for i in response.json["data"] if i["status"] == "pending"][0] + award2_id = award['id'] + self.assertEqual(award['bid_id'], bid_1) + self.assertEqual(award['value']['amount'], 450) + self.assertNotEqual(award_id, award2_id) + + # get awards + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}/awards?acc_token={}".format(tender_id, owner_token)) + # get pending award + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # set award as active + self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, bid_token1), + {"data": {"status": "active"}} + ) + # get contract id + response = self.app.get("/tenders/{}".format(tender_id)) + contract_id = response.json["data"]["contracts"][-1]["id"] + # create tender contract document for test + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(tender_id, contract_id, owner_token), + upload_files=[("file", "name.doc", "content")], + status=201, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + # sign contract + self.app.authorization = ("Basic", ("broker", "")) + self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(tender_id, contract_id, owner_token), + {"data": {"status": "active", "value": {"valueAddedTaxIncluded": False}}}, + ) + # check status + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.json["data"]["status"], "complete") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(tender_id, contract_id, owner_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't add document in current (complete) tender status" + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format(tender_id, contract_id, doc_id, owner_token), + {"data": {"description": "document description"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update document in current (complete) tender status" + ) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format(tender_id, contract_id, doc_id, owner_token), + upload_files=[("file", "name.doc", "content3")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], "Can't update document in current (complete) tender status" + ) + + +def lost_contract_for_active_award(self): + tender_id = self.tender_id + owner_token = self.tender_token + # create bid + self.app.authorization = ("Basic", ("broker", "")) + resp = self.app.post_json( + "/tenders/{}/bids".format(tender_id), {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid + }} + ) + token = resp.json['access']['token'] + # switch to active.qualification + self.set_status("active.tendering", 'end') + resp = self.check_chronograph().json + self.assertEqual(resp['data']['status'], 'active.qualification') + + # get awards + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}/awards?acc_token={}".format(tender_id, owner_token)) + # get pending award + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # set award as active + self.app.patch_json( + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, token), {"data": {"status": "active"}} + ) + # lost contract + tender = self.db.get(tender_id) + tender["contracts"] = None + self.db.save(tender) + # check tender + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.json["data"]["status"], "active.awarded") + self.assertNotIn("contracts", response.json["data"]) + self.assertIn("next_check", response.json["data"]) + # create lost contract + self.app.authorization = ("Basic", ("chronograph", "")) + response = self.app.patch_json("/tenders/{}".format(tender_id), {"data": {"id": tender_id}}) + self.assertEqual(response.json["data"]["status"], "active.awarded") + self.assertIn("contracts", response.json["data"]) + self.assertNotIn("next_check", response.json["data"]) + contract_id = response.json["data"]["contracts"][-1]["id"] + # sign contract + self.app.authorization = ("Basic", ("broker", "")) + self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(tender_id, contract_id, owner_token), + {"data": {"status": "active", "value": {"valueAddedTaxIncluded": False}}}, + ) + # check status + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.get("/tenders/{}".format(tender_id)) + self.assertEqual(response.json["data"]["status"], "complete") + + diff --git a/src/openprocurement/tender/pricequotation/tests/tests.ini b/src/openprocurement/tender/pricequotation/tests/tests.ini new file mode 100644 index 0000000000..207b95f1f7 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/tests/tests.ini @@ -0,0 +1,28 @@ +[app:main] +use = egg:openprocurement.api + +couchdb.db_name = tests_tender_pricequotation +couchdb.url = http://op:op@couchdb:5984/ + +auth.file = %(here)s/../../../api/tests/auth.ini + +pyramid.reload_templates = true +pyramid.debug_authorization = true +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.debug_templates = true +pyramid.default_locale_name = en +plugins = + api, + tender.core, + tender.pricequotation +update_after = false + +dockey = 480310b588d10049d4a1199c37c258ecc9e2d15fde6851cbe8eaf35210fbefc0 +dockeys = a8968c4682ffa921c91caab5b60c84fbd089311549e5c3defd48f413c89337b6 + +[server:main] +use = egg:chaussette +host = 0.0.0.0 +port = 6543 +backend = gevent diff --git a/src/openprocurement/tender/pricequotation/utils.py b/src/openprocurement/tender/pricequotation/utils.py new file mode 100644 index 0000000000..5380b7a486 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/utils.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +from collections import defaultdict +from datetime import timedelta +from logging import getLogger +from pyramid.security import Allow +from openprocurement.api.constants import RELEASE_2020_04_19 +from openprocurement.api.utils import get_now, context_unpack +from openprocurement.tender.core.utils import ( + remove_draft_bids, + calculate_tender_business_date, +) + +from openprocurement.tender.core.utils import get_first_revision_date +from openprocurement.tender.belowthreshold.utils import add_contract +from openprocurement.tender.pricequotation.constants import QUALIFICATION_DURATION + + +LOGGER = getLogger("openprocurement.tender.pricequotation") + + +def check_bids(request): + tender = request.validated["tender"] + pending_cancellations = [ + i.status not in ["active", "unsuccessful"] + for i in tender.cancellations + ] + if any(pending_cancellations): + return + if tender.numberOfBids == 0: + tender.status = "unsuccessful" + else: + add_next_award(request) + + +def cancel_tender(request): + tender = request.validated["tender"] + if tender.status in ["active.tendering"]: + tender.bids = [] + tender.status = "cancelled" + + +def check_award_status(request): + tender = request.validated["tender"] + now = get_now() + awards = tender.awards + for award in awards: + if award.status == 'pending' and\ + calculate_tender_business_date(award.date, + QUALIFICATION_DURATION, + tender) <= now: + award.status = 'unsuccessful' + add_next_award(request) + if award.status == "active" and not any([i.awardID == award.id for i in tender.contracts]): + add_contract(request, award, now) + add_next_award(request) + + +def check_status(request): + + check_award_status(request) + + tender = request.validated["tender"] + now = get_now() + + if tender.status == "active.tendering" and tender.tenderPeriod.endDate <= now: + tender.status = "active.qualification" + remove_draft_bids(request) + check_bids(request) + status = tender.status + LOGGER.info( + "Switched tender {} to {}".format(tender["id"], status), + extra=context_unpack(request, + {"MESSAGE_ID": "switched_tender_{}".format(status)}), + ) + return + elif tender.status == "active.awarded": + check_tender_status(request) + + +def check_tender_status(request): + tender = request.validated["tender"] + last_award_status = tender.awards[-1].status if tender.awards else "" + if last_award_status == "unsuccessful": + LOGGER.info( + "Switched tender {} to {}".format(tender.id, "unsuccessful"), + extra=context_unpack( + request, + {"MESSAGE_ID": "switched_tender_unsuccessful"} + ), + ) + tender.status = "unsuccessful" + if tender.contracts and tender.contracts[-1].status == "active": + tender.status = "complete" + + +def add_next_award(request): + tender = request.validated["tender"] + now = get_now() + if not tender.awardPeriod: + tender.awardPeriod = type(tender).awardPeriod({}) + if not tender.awardPeriod.startDate: + tender.awardPeriod.startDate = now + if not tender.awards or tender.awards[-1].status not in ["pending", "active"]: + unsuccessful_awards = [ + a.bid_id for a in tender.awards + if a.status == "unsuccessful" + ] + bids = sorted([ + bid for bid in tender.bids + if bid.id not in unsuccessful_awards + ], key=lambda bid: bid.value.amount) + if bids: + bid = bids[0].serialize() + award = type(tender).awards.model_class( + { + "bid_id": bid["id"], + "status": "pending", + "date": get_now(), + "value": bid["value"], + "suppliers": bid["tenderers"], + } + ) + award.__parent__ = tender + tender.awards.append(award) + request.response.headers["Location"] = request.route_url( + "{}:Tender Awards".format(tender.procurementMethodType), + tender_id=tender.id, + award_id=award["id"] + ) + else: + tender.status = 'unsuccessful' + return + if tender.awards[-1].status == "pending": + tender.awardPeriod.endDate = None + tender.status = "active.qualification" + else: + tender.awardPeriod.endDate = now + tender.status = "active.awarded" + + +def get_bid_owned_award_acl(award): + acl = [] + if not hasattr(award, "__parent__") or 'bids' not in award.__parent__: + return acl + tender = award.__parent__ + awarded_bid = [bid for bid in tender.bids if bid.id == award.bid_id][0] + prev_awards = [ + a for a in tender.awards + if a.bid_id == awarded_bid.id and + a.id != award.id and + a['status'] != 'pending' + ] + bid_acl = "_".join((awarded_bid.owner, awarded_bid.owner_token)) + owner_acl = "_".join((tender.owner, tender.owner_token)) + + if prev_awards or award.status != 'pending': + acl.extend([ + (Allow, owner_acl, "upload_award_documents"), + (Allow, owner_acl, "edit_award") + ]) + else: + acl.extend([ + (Allow, bid_acl, "upload_award_documents"), + (Allow, bid_acl, "edit_award") + ]) + return acl + + +def find_parent(id_): + parts = id_.split('-') + return '-'.join(parts[:-1]) + + +def requirements_to_tree(requirements): + return { + requirement['id']: requirement + for requirement in requirements + } + + +def group_to_tree(groups): + return { + group['id']: requirements_to_tree(group['requirements']) + for group in groups + } + + +def criteria_to_tree(criterias): + return { + criteria['id']: group_to_tree(criteria['requirementGroups']) + for criteria in criterias + } + + +def responses_to_tree(responses): + groups = defaultdict(dict) + for response in responses: + groups[find_parent(response.requirement.id)][response['requirement']['id']] = response + + criterias = defaultdict(dict) + for group_id, group in groups.items(): + criterias[find_parent(group_id)][group_id] = group + return criterias diff --git a/src/openprocurement/tender/pricequotation/validation.py b/src/openprocurement/tender/pricequotation/validation.py new file mode 100644 index 0000000000..12162ac9ac --- /dev/null +++ b/src/openprocurement/tender/pricequotation/validation.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +from schematics.types import DecimalType, StringType, IntType, BooleanType +from schematics.exceptions import ValidationError + +from openprocurement.api.utils import error_handler, raise_operation_error +from openprocurement.api.validation import\ + validate_data, OPERATIONS, validate_json_data +from openprocurement.tender.pricequotation.utils import\ + responses_to_tree, criteria_to_tree + + +TYPEMAP = { + 'string': StringType(), + 'integer': IntType(), + 'number': DecimalType(), + 'boolean': BooleanType() +} + + +# tender documents +def validate_document_operation_in_not_allowed_period(request): + if request.validated["tender_status"] not in ["active.tendering", "draft"]: + raise_operation_error( + request, + "Can't {} document in current ({}) tender status".format( + OPERATIONS.get(request.method), request.validated["tender_status"] + ), + ) + + +# award +def validate_create_award_not_in_allowed_period(request): + tender = request.validated["tender"] + if tender.status != "active.qualification": + raise_operation_error( + request, + "Can't create award in current ({}) tender status".format( + tender.status + ) + ) + + +def validate_award_update_in_terminal_status(request): + award_status = request.validated['award'].status + if award_status in ('cancelled', 'unsuccessful'): + raise_operation_error( + request, + "Can't update award in current ({}) status".format( + award_status + ) + ) + + +# contract document +def validate_contract_document(request): + operation = OPERATIONS.get(request.method) + if request.validated["tender_status"] not in\ + ["active.qualification", "active.awarded"]: + raise_operation_error( + request, + "Can't {} document in current ({}) tender status".format( + operation, request.validated["tender_status"] + ), + ) + if request.validated["contract"].status not in ["pending", "active"]: + raise_operation_error( + request, + "Can't {} document in current contract status".format(operation) + ) + return True + + +def validate_award_document(request): + operation = OPERATIONS.get(request.method) + + allowed_tender_statuses = ["active.qualification"] + if request.authenticated_role == "bots": + allowed_tender_statuses.append("active.awarded") + if request.validated["tender_status"] not in allowed_tender_statuses: + raise_operation_error( + request, + "Can't {} document in current ({}) tender status".format( + operation, request.validated["tender_status"] + ), + ) + + if operation == "update" and request.authenticated_role != (request.context.author or "tender_owner"): + request.errors.add("url", "role", "Can update document only author") + request.errors.status = 403 + raise error_handler(request.errors) + + +def validate_patch_tender_data(request): + model = type(request.tender) + data = validate_data(request, model, True, validate_json_data(request)) + validate_kind_update(request, model) + return data + + +def validate_kind_update(request, model): + data = request.validated["data"] + kind = data.get("procuringEntity", {}).get("kind", "") + if kind and kind not in model.procuring_entity_kinds: + request.errors.add( + "procuringEntity", "kind", + "{kind!r} procuringEntity cannot publish this type of procedure. Only {kinds} are allowed.".format( + kind=kind, kinds=", ".join(model.procuring_entity_kinds) + ) + ) + request.errors.status = 403 + + +def validate_bid_value(tender, value): + if not value: + raise ValidationError(u"This field is required.") + if tender.value.amount < value.amount: + raise ValidationError(u"value of bid should be less than value of tender") + if tender.get("value").currency != value.currency: + raise ValidationError(u"currency of bid should be identical to currency of value of tender") + if tender.get("value").valueAddedTaxIncluded != value.valueAddedTaxIncluded: + raise ValidationError( + u"valueAddedTaxIncluded of bid should be identical " u"to valueAddedTaxIncluded of value of tender" + ) + + +# tender.criterion.requirementGrpoups +def validate_requirement_groups(value): + for requirements in value: + for requirement in requirements.requirements: + expected = requirement.get('expectedValue') + min_value = requirement.get('minValue') + max_value = requirement.get('maxValue') + if not any((expected, min_value, max_value)): + raise ValidationError( + u'Value required for at least one field ["expectedValue", "minValue", "maxValue"]' + ) + if any((expected and min_value, expected and max_value)): + raise ValidationError( + u'expectedValue conflicts with ["minValue", "maxValue"]' + ) + + +def validate_value_type(value, datatype): + if not value: + return + type_ = TYPEMAP.get(datatype) + if not type_: + raise ValidationError( + u'Type mismatch: value {} does not confront type {}'.format( + value, type_ + ) + ) + # validate value + type_.to_native(value) + + +# bid.requirementResponeses +def matches(criteria, response): + datatype = TYPEMAP[criteria['dataType']] + # validate value + value = datatype.to_native(response['value']) + + expected = criteria.get('expectedValue') + min_value = criteria.get('minValue') + max_value = criteria.get('maxValue') + + if expected: + expected = datatype.to_native(expected) + if datatype.to_native(expected) != value: + raise ValidationError( + u'Value "{}" does not match expected value "{}" in reqirement {}'.format( + value, expected, criteria['id'] + ) + ) + if min_value and max_value: + min_value = datatype.to_native(min_value) + max_value = datatype.to_native(max_value) + if (value < min_value) or (value > max_value): + raise ValidationError( + u'Value "{}" does not match range from "{}" to "{}" in reqirement {}'.format( + value, + min_value, + max_value, + criteria['id'] + ) + ) + + if min_value and not max_value: + min_value = datatype.to_native(min_value) + if value < min_value: + raise ValidationError( + u'Value {} is lower then minimal required {} in reqirement {}'.format( + value, + min_value, + criteria['id'] + ) + ) + if not min_value and max_value: + if value > datatype.to_native(max_value): + raise ValidationError( + u'Value {} is higher then required {} in reqirement {}'.format( + value, + max_value, + criteria['id'] + ) + ) + return response + + +def validate_requirement_responses(criterias, req_responses): + criterias = criteria_to_tree(criterias) + responses = responses_to_tree(req_responses) + # top level criterias. all required + diff = set(criterias).difference(responses) + if diff: + raise ValidationError(u'Missing references for criterias: {}'.format( + list(diff) + )) + + for criteria_id, group_response in responses.items(): + # OR for requirementGroup + if len(group_response) > 1: + raise ValidationError( + u'Provided groups {} conflicting in criteria {}'.format( + group_response.keys(), criteria_id + )) + criteria_groups = criterias[criteria_id] + for group_id, requirements in criteria_groups.items(): + if group_id not in group_response: + continue + # response satisfies requirement + responses = group_response.get(group_id, set()) + diff = set(requirements).difference(responses) + if diff: + raise ValidationError( + u'Missing references for reqirements: {}'.format( + list(diff) + )) + for response_id, response in responses.items(): + matches(requirements[response_id], response) + + +def validate_tender_publish(request): + current_status = request.validated['tender'].status + tender_status = request.validated['data'].get('status', current_status) + if tender_status == current_status: + return + if request.authenticated_role not in ("bots", "Administrator", "chronograph") \ + and tender_status != "draft.publishing": + raise_operation_error(request, + "{} can't switch tender from status ({}) to ({})".format(request.authenticated_role, + current_status, + tender_status)) diff --git a/src/openprocurement/tender/pricequotation/views/__init__.py b/src/openprocurement/tender/pricequotation/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openprocurement/tender/pricequotation/views/award.py b/src/openprocurement/tender/pricequotation/views/award.py new file mode 100644 index 0000000000..7671fe5469 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/award.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.utils import\ + get_now, json_view, context_unpack, raise_operation_error +from openprocurement.tender.core.utils import\ + optendersresource, save_tender, apply_patch +from openprocurement.tender.belowthreshold.views.award import\ + TenderAwardResource +from openprocurement.tender.pricequotation.utils import\ + add_next_award, add_contract +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.core.validation import ( + validate_award_data, + validate_patch_award_data, + validate_update_award_in_not_allowed_status, +) +from openprocurement.tender.pricequotation.validation import ( + validate_create_award_not_in_allowed_period, + validate_award_update_in_terminal_status, +) + + +@optendersresource( + name="{}:Tender Awards".format(PMT), + collection_path="/tenders/{tender_id}/awards", + path="/tenders/{tender_id}/awards/{award_id}", + description="Tender awards", + procurementMethodType=PMT, +) +class PQTenderAwardResource(TenderAwardResource): + """ PriceQuotation award resource """ + @json_view( + content_type="application/json", + permission="create_award", + validators=( + validate_award_data, + validate_create_award_not_in_allowed_period, + ), + ) + def collection_post(self): + tender = self.request.validated["tender"] + award = self.request.validated["award"] + tender.awards.append(award) + if save_tender(self.request): + self.LOGGER.info( + "Created tender award {}".format(award.id), + extra=context_unpack( + self.request, + {"MESSAGE_ID": "tender_award_create"}, + {"award_id": award.id} + ), + ) + self.request.response.status = 201 + self.request.response.headers["Location"] = self.request.route_url( + "{}:Tender Awards".format(tender.procurementMethodType), + tender_id=tender.id, award_id=award["id"] + ) + + return {"data": award.serialize("view")} + + @json_view( + content_type="application/json", + permission="edit_award", + validators=( + validate_patch_award_data, + validate_update_award_in_not_allowed_status, + validate_award_update_in_terminal_status + ), + ) + def patch(self): + tender = self.request.validated["tender"] + award = self.request.context + is_awarded = [ + a for a in tender.awards + if a.bid_id == award.bid_id and a.id != award.id + ] + award_status = award.status + apply_patch(self.request, save=False, src=self.request.context.serialize()) + + now = get_now() + if is_awarded and award.status != 'unsuccessful': + raise_operation_error( + self.request, + "Can't change award status to {} from {}".format(award.status, award_status) + ) + + if award_status == "pending" and award.status == "active": + add_contract(self.request, award, now) + add_next_award(self.request) + elif award_status == "active" and award.status == "cancelled": + for i in tender.contracts: + if i.awardID == award.id: + i.status = "cancelled" + add_next_award(self.request) + elif award_status == "pending" and award.status == "unsuccessful": + if is_awarded: + tender.status = 'unsuccessful' + else: + add_next_award(self.request) + elif self.request.authenticated_role != "Administrator" and not ( + award_status == "pending" and award.status == "pending" + ): + raise_operation_error( + self.request, + "Can't update award in current ({}) status".format(award_status) + ) + if save_tender(self.request): + self.LOGGER.info( + "Updated tender award {}".format(self.request.context.id), + extra=context_unpack( + self.request, + {"MESSAGE_ID": "tender_award_patch"} + ), + ) + return {"data": award.serialize("view")} diff --git a/src/openprocurement/tender/pricequotation/views/award_document.py b/src/openprocurement/tender/pricequotation/views/award_document.py new file mode 100644 index 0000000000..1cb918976c --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/award_document.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.utils import json_view +from openprocurement.api.validation import\ + validate_file_update, validate_file_upload, validate_patch_document_data +from openprocurement.tender.core.utils import optendersresource +from openprocurement.tender.belowthreshold.views.award_document import\ + TenderAwardDocumentResource +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.pricequotation.validation import\ + validate_award_document + + +@optendersresource( + name="{}:Tender Award Documents".format(PMT), + collection_path="/tenders/{tender_id}/awards/{award_id}/documents", + path="/tenders/{tender_id}/awards/{award_id}/documents/{document_id}", + procurementMethodType=PMT, + description="Tender award documents", +) +class PQTenderAwardDocumentResource(TenderAwardDocumentResource): + """ PriceQuotation award document resource """ + + @json_view( + validators=(validate_file_upload, validate_award_document), + permission="upload_award_documents" + ) + def collection_post(self): + return super(TenderAwardDocumentResource, self).collection_post() + + @json_view( + validators=(validate_file_update, validate_award_document), + permission="upload_award_documents" + ) + def put(self): + return super(TenderAwardDocumentResource, self).put() + + @json_view( + content_type="application/json", + validators=(validate_patch_document_data, validate_award_document), + permission="upload_award_documents", + ) + def patch(self): + return super(TenderAwardDocumentResource, self).patch() diff --git a/src/openprocurement/tender/pricequotation/views/bid.py b/src/openprocurement/tender/pricequotation/views/bid.py new file mode 100644 index 0000000000..cfa1cca2a8 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/bid.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.validation import OPERATIONS +from openprocurement.api.utils import\ + get_now, json_view, context_unpack, raise_operation_error +from openprocurement.tender.core.utils import optendersresource, apply_patch +from openprocurement.tender.core.validation import ( + validate_patch_bid_data, + validate_bid_operation_period, + validate_bid_operation_not_in_tendering, +) + +from openprocurement.tender.belowthreshold.views.bid import\ + TenderBidResource as BaseTenderBidResource +from openprocurement.tender.belowthreshold.validation import\ + validate_update_bid_status +from openprocurement.tender.pricequotation.constants import PMT + + +@optendersresource( + name="{}:Tender Bids".format(PMT), + collection_path="/tenders/{tender_id}/bids", + path="/tenders/{tender_id}/bids/{bid_id}", + procurementMethodType=PMT, + description="Tender bids", +) +class TenderBidResource(BaseTenderBidResource): + """ PriceQuotation tender bid resource """ + @json_view( + content_type="application/json", + permission="edit_bid", + validators=( + validate_patch_bid_data, + validate_bid_operation_not_in_tendering, + validate_bid_operation_period, + validate_update_bid_status, + ), + ) + def patch(self): + value = self.request.validated["data"].get("value") and self.request.validated["data"]["value"].get("amount") + if value and value != self.request.context.get("value", {}).get("amount"): + self.request.validated["data"]["date"] = get_now().isoformat() + self.request.validated["tender"].modified = False + if apply_patch(self.request, src=self.request.context.serialize()): + self.LOGGER.info( + "Updated tender bid {}".format(self.request.context.id), + extra=context_unpack(self.request, {"MESSAGE_ID": "tender_bid_patch"}), + ) + return {"data": self.request.context.serialize("view")} + + @json_view( + permission="edit_bid", + validators=( + validate_bid_operation_not_in_tendering, + validate_bid_operation_period + ) + ) + def delete(self): + """ + Cancelling the proposal. + Forbidden for price quotation tender. + """ + request = self.request + raise_operation_error( + request, + "Can't {} bid in Price Quotation tender".format( + OPERATIONS.get(request.method), + ), + ) diff --git a/src/openprocurement/tender/pricequotation/views/bid_document.py b/src/openprocurement/tender/pricequotation/views/bid_document.py new file mode 100644 index 0000000000..0c6660953d --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/bid_document.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from openprocurement.tender.core.views.bid_document import\ + TenderBidDocumentResource +from openprocurement.tender.core.utils import optendersresource +from openprocurement.tender.pricequotation.constants import PMT + + +@optendersresource( + name="{}:Tender Bid Documents".format(PMT), + collection_path="/tenders/{tender_id}/bids/{bid_id}/documents", + path="/tenders/{tender_id}/bids/{bid_id}/documents/{document_id}", + procurementMethodType=PMT, + description="Tender bidder documents", +) +class PQTenderBidDocumentResource(TenderBidDocumentResource): + pass diff --git a/src/openprocurement/tender/pricequotation/views/cancellation.py b/src/openprocurement/tender/pricequotation/views/cancellation.py new file mode 100644 index 0000000000..27eded2f83 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/cancellation.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.utils import json_view, get_now,\ + context_unpack +from openprocurement.tender.core.utils import\ + optendersresource, save_tender, apply_patch +from openprocurement.tender.belowthreshold.views.cancellation import\ + TenderCancellationResource +from openprocurement.tender.core.validation import ( + validate_tender_not_in_terminated_status, + validate_cancellation_data, + validate_patch_cancellation_data, + validate_cancellation_status_without_complaints +) +from openprocurement.tender.pricequotation.utils import cancel_tender +from openprocurement.tender.pricequotation.constants import PMT + + +@optendersresource( + name="{}:Tender Cancellations".format(PMT), + collection_path="/tenders/{tender_id}/cancellations", + path="/tenders/{tender_id}/cancellations/{cancellation_id}", + procurementMethodType=PMT, + description="Tender cancellations", +) +class PQTenderCancellationResource(TenderCancellationResource): + """PriceQuotation cancellation""" + + @json_view( + content_type="application/json", + validators=( + validate_tender_not_in_terminated_status, + validate_cancellation_data, + ), + permission="edit_tender" + ) + def collection_post(self): + cancellation = self.request.validated["cancellation"] + cancellation.date = get_now() + + self.request.context.cancellations.append(cancellation) + if save_tender(self.request): + self.LOGGER.info( + "Created tender cancellation {}".format(cancellation.id), + extra=context_unpack( + self.request, {"MESSAGE_ID": "tender_cancellation_create"}, {"cancellation_id": cancellation.id} + ), + ) + self.request.response.status = 201 + self.request.response.headers["Location"] = self.request.route_url( + "{}:Tender Cancellations".format(self.request.validated["tender"].procurementMethodType), + tender_id=self.request.validated["tender_id"], + cancellation_id=cancellation.id, + ) + return {"data": cancellation.serialize("view")} + + @json_view( + content_type="application/json", + validators=( + validate_patch_cancellation_data, + validate_cancellation_status_without_complaints, + validate_tender_not_in_terminated_status, + ), + permission="edit_cancellation" + ) + def patch(self): + cancellation = self.request.context + prev_status = cancellation.status + apply_patch(self.request, save=False, src=cancellation.serialize()) + + if cancellation.status == "active" and prev_status != "active": + cancel_tender(self.request) + + if save_tender(self.request): + self.LOGGER.info( + "Updated tender cancellation {}".format(cancellation.id), + extra=context_unpack( + self.request, + {"MESSAGE_ID": "tender_cancellation_patch"} + ), + ) + return {"data": cancellation.serialize("view")} diff --git a/src/openprocurement/tender/pricequotation/views/cancellation_document.py b/src/openprocurement/tender/pricequotation/views/cancellation_document.py new file mode 100644 index 0000000000..4a70a13d28 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/cancellation_document.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from openprocurement.tender.core.utils import optendersresource +from openprocurement.tender.belowthreshold.views.cancellation_document import\ + TenderCancellationDocumentResource + +from openprocurement.tender.pricequotation.constants import PMT + + +@optendersresource( + name="{}:Tender Cancellation Documents".format(PMT), + collection_path="/tenders/{tender_id}/cancellations/{cancellation_id}/documents", + path="/tenders/{tender_id}/cancellations/{cancellation_id}/documents/{document_id}", + procurementMethodType=PMT, + description="Tender cancellation documents", +) +class TenderCancellationDocumentResource(TenderCancellationDocumentResource): + """ PriceQuotation cancellation document """ diff --git a/src/openprocurement/tender/pricequotation/views/contract.py b/src/openprocurement/tender/pricequotation/views/contract.py new file mode 100644 index 0000000000..2193c2148c --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/contract.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.utils import\ + json_view, raise_operation_error, context_unpack, get_now +from openprocurement.tender.core.utils import optendersresource,\ + apply_patch, save_tender +from openprocurement.tender.core.validation import ( + validate_patch_contract_data, + validate_update_contract_value, + validate_contract_operation_not_in_allowed_status, + validate_update_contract_value_with_award, + validate_update_contract_value_amount, + validate_update_contract_value_net_required, + validate_update_contract_status_by_supplier +) +from openprocurement.tender.belowthreshold.views.contract\ + import TenderAwardContractResource +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.pricequotation.utils import check_tender_status + + +@optendersresource( + name="{}:Tender Contracts".format(PMT), + collection_path="/tenders/{tender_id}/contracts", + procurementMethodType=PMT, + path="/tenders/{tender_id}/contracts/{contract_id}", + description="Tender contracts", +) +class PQTenderAwardContractResource(TenderAwardContractResource): + """""" + @json_view( + content_type="application/json", + permission="edit_contract", + validators=( + validate_patch_contract_data, + validate_contract_operation_not_in_allowed_status, + validate_update_contract_status_by_supplier, + validate_update_contract_value, + validate_update_contract_value_net_required, + validate_update_contract_value_with_award, + validate_update_contract_value_amount, + ), + ) + def patch(self): + """Update of contract + """ + contract_status = self.request.context.status + apply_patch(self.request, save=False, src=self.request.context.serialize()) + if contract_status != self.request.context.status and \ + (contract_status not in ("pending", "pending.winner-signing",) or \ + self.request.context.status not in ("active", "pending", "pending.winner-signing",)): + raise_operation_error(self.request, "Can't update contract status") + if self.request.context.status == "active" and not self.request.context.dateSigned: + self.request.context.dateSigned = get_now() + check_tender_status(self.request) + if save_tender(self.request): + self.LOGGER.info( + "Updated tender contract {}".format(self.request.context.id), + extra=context_unpack(self.request, {"MESSAGE_ID": "tender_contract_patch"}), + ) + return {"data": self.request.context.serialize()} diff --git a/src/openprocurement/tender/pricequotation/views/contract_document.py b/src/openprocurement/tender/pricequotation/views/contract_document.py new file mode 100644 index 0000000000..2185870b99 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/contract_document.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.utils import ( + upload_file, + update_file_content_type, + json_view, + context_unpack, +) +from openprocurement.api.validation import\ + validate_file_update, validate_patch_document_data, validate_file_upload +from openprocurement.tender.core.utils import\ + save_tender, optendersresource, apply_patch +from openprocurement.tender.core.validation import\ + validate_role_for_contract_document_operation +from openprocurement.tender.belowthreshold.views.contract_document\ + import TenderAwardContractDocumentResource +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.pricequotation.validation import\ + validate_contract_document + + +@optendersresource( + name="{}:Tender Contract Documents".format(PMT), + collection_path="/tenders/{tender_id}/contracts/{contract_id}/documents", + path="/tenders/{tender_id}/contracts/{contract_id}/documents/{document_id}", + procurementMethodType=PMT, + description="Tender contract documents", +) +class PQTenderAwardContractDocumentResource(TenderAwardContractDocumentResource): + + @json_view( + permission="upload_contract_documents", + validators=( + validate_file_upload, + validate_role_for_contract_document_operation, + validate_contract_document, + ) + ) + def collection_post(self): + """Tender Contract Document Upload + """ + document = upload_file(self.request) + self.context.documents.append(document) + if save_tender(self.request): + self.LOGGER.info( + "Created tender contract document {}".format(document.id), + extra=context_unpack( + self.request, + {"MESSAGE_ID": "tender_contract_document_create"}, + {"document_id": document.id} + ), + ) + self.request.response.status = 201 + document_route = self.request.matched_route.name.replace("collection_", "") + self.request.response.headers["Location"] = self.request.current_route_url( + _route_name=document_route, document_id=document.id, _query={} + ) + return {"data": document.serialize("view")} + + @json_view( + validators=( + validate_file_update, + validate_role_for_contract_document_operation, + validate_contract_document, + ), + permission="upload_contract_documents" + ) + def put(self): + """Tender Contract Document Update""" + document = upload_file(self.request) + self.request.validated["contract"].documents.append(document) + if save_tender(self.request): + self.LOGGER.info( + "Updated tender contract document {}".format(self.request.context.id), + extra=context_unpack( + self.request, + {"MESSAGE_ID": "tender_contract_document_put"} + ), + ) + return {"data": document.serialize("view")} + + @json_view( + content_type="application/json", + validators=( + validate_patch_document_data, + validate_role_for_contract_document_operation, + validate_contract_document, + ), + permission="upload_contract_documents" + ) + def patch(self): + """Tender Contract Document Update""" + if apply_patch(self.request, src=self.request.context.serialize()): + update_file_content_type(self.request) + self.LOGGER.info( + "Updated tender contract document {}".format(self.request.context.id), + extra=context_unpack( + self.request, + {"MESSAGE_ID": "tender_contract_document_patch"} + ), + ) + return {"data": self.request.context.serialize("view")} diff --git a/src/openprocurement/tender/pricequotation/views/tender.py b/src/openprocurement/tender/pricequotation/views/tender.py new file mode 100644 index 0000000000..446b020416 --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/tender.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from datetime import timedelta +from openprocurement.api.utils import context_unpack, json_view, get_now +from openprocurement.tender.core.utils import\ + save_tender, optendersresource, apply_patch +from openprocurement.tender.core.validation import\ + validate_tender_not_in_terminated_status + +from openprocurement.tender.belowthreshold.views.tender import TenderResource +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.pricequotation.utils import check_status +from openprocurement.tender.pricequotation.validation import\ + validate_patch_tender_data, validate_tender_publish + + +@optendersresource( + name="{}:Tender".format(PMT), + path="/tenders/{tender_id}", + procurementMethodType=PMT, +) +class PriceQuotationTenderResource(TenderResource): + """ + PriceQuotation tender creation and updation + """ + @json_view( + content_type="application/json", + validators=( + validate_patch_tender_data, + validate_tender_publish, + validate_tender_not_in_terminated_status, + ), + permission="edit_tender", + ) + def patch(self): + tender = self.context + if self.request.authenticated_role == "chronograph": + apply_patch(self.request, save=False, src=self.request.validated["tender_src"]) + check_status(self.request) + save_tender(self.request) + else: + new_status = self.request.validated["data"].get("status", "") + data = self.request.validated["data"] + if tender.status == "draft" and new_status == "draft.publishing" and not tender.noticePublicationDate: + now = get_now() + self.request.validated["data"]["noticePublicationDate"] = now.isoformat() + self.request.validated["data"]["tenderPeriod"]["startDate"] = now.isoformat() + apply_patch(self.request, src=self.request.validated["tender_src"]) + self.LOGGER.info( + "Updated tender {}".format(tender.id), extra=context_unpack(self.request, {"MESSAGE_ID": "tender_patch"}) + ) + return {"data": tender.serialize(tender.status)} diff --git a/src/openprocurement/tender/pricequotation/views/tender_document.py b/src/openprocurement/tender/pricequotation/views/tender_document.py new file mode 100644 index 0000000000..2497a04b3d --- /dev/null +++ b/src/openprocurement/tender/pricequotation/views/tender_document.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from openprocurement.api.utils import json_view +from openprocurement.api.validation import validate_patch_document_data,\ + validate_file_upload, validate_file_update +from openprocurement.tender.belowthreshold.views.tender_document import\ + TenderDocumentResource +from openprocurement.tender.core.utils import optendersresource +from openprocurement.tender.pricequotation.constants import PMT +from openprocurement.tender.core.validation import\ + validate_tender_document_update_not_by_author_or_tender_owner +from openprocurement.tender.pricequotation.validation import validate_document_operation_in_not_allowed_period + + +@optendersresource( + name="{}:Tender Documents".format(PMT), + collection_path="/tenders/{tender_id}/documents", + path="/tenders/{tender_id}/documents/{document_id}", + procurementMethodType=PMT, + description="Tender related binary files (PDFs, etc.)", +) +class PQTenderDocumentResource(TenderDocumentResource): + + @json_view( + permission="upload_tender_documents", + validators=( + validate_file_upload, + validate_document_operation_in_not_allowed_period + ), + ) + def collection_post(self): + return super(PQTenderDocumentResource, self).collection_post() + + @json_view( + permission="upload_tender_documents", + validators=( + validate_file_update, + validate_document_operation_in_not_allowed_period, + validate_tender_document_update_not_by_author_or_tender_owner + ), + ) + def put(self): + """Tender Document Update""" + return super(PQTenderDocumentResource, self).put() + + @json_view( + content_type="application/json", + permission="upload_tender_documents", + validators=( + validate_patch_document_data, + validate_document_operation_in_not_allowed_period, + validate_tender_document_update_not_by_author_or_tender_owner + ), + ) + def patch(self): + """Tender Document Update""" + return super(PQTenderDocumentResource, self).patch()