Repository: SebRut/pygrocy (python)

Issue:
Get product by Barcode
First of all, very cool project, it seems to make very straight forward to integrate grocy with other systems.

I would like to add/consume products by scanning their barcodes unfortunately I haven't found a way to retrieve the product_id by giving the barcode, is it that possible?

In the [grocy-pyscanner](https://github.com/grocy/grocy-pyscanner/blob/09e62fa8b39d14f304e71edd46fb5e1ab095cc52/barcode_reader.py#L94) they do it by a get request, so it is possible bu I haven't found how to do it in pygrocy. 
Thanks a lot!

Pull request description:
Product by barcode
## Description

This PR adds methods to get, add and consume products by barcode, as well as associated tests.

**Related issue (if applicable):** fixes #85 

## Checklist
  - [x] The code change is tested and works locally.
  - [x] tests pass. **Your PR won't be merged unless tests pass**
  - [x] There is no commented out code in this PR


Patch:
diff --git a/pygrocy/data_models/product.py b/pygrocy/data_models/product.py
index 8f6ede0..9242fd6 100644
--- a/pygrocy/data_models/product.py
+++ b/pygrocy/data_models/product.py
@@ -11,6 +11,7 @@ from pygrocy.grocy_api_client import (
     ProductData,
     ProductDetailsResponse,
     ShoppingListItem,
+    StockLogResponse,
 )
 
 
@@ -34,6 +35,8 @@ class Product(DataModel):
             self._init_from_ProductDetailsResponse(data)
         elif isinstance(data, ProductData):
             self._init_from_ProductData(data)
+        elif isinstance(data, StockLogResponse):
+            self._init_from_StockLogResponse(data)
 
     def _init_empty(self):
         self._name = None
@@ -74,6 +77,9 @@ class Product(DataModel):
         self._product_group_id = product.product_group_id
         self._name = product.name
 
+    def _init_from_StockLogResponse(self, response: StockLogResponse):
+        self._id = response.product_id
+
     def get_details(self, api_client: GrocyApiClient):
         details = api_client.get_product(self.id)
         if details:
diff --git a/pygrocy/grocy.py b/pygrocy/grocy.py
index cd1a559..880a76a 100644
--- a/pygrocy/grocy.py
+++ b/pygrocy/grocy.py
@@ -97,6 +97,11 @@ class Grocy(object):
         if resp:
             return Product(resp)
 
+    def product_by_barcode(self, barcode: str) -> Product:
+        resp = self._api_client.get_product_by_barcode(barcode)
+        if resp:
+            return Product(resp)
+
     def all_products(self) -> List[Product]:
         raw_products = self.get_generic_objects_for_type(EntityType.PRODUCTS)
         from pygrocy.grocy_api_client import ProductData
@@ -148,6 +153,39 @@ class Grocy(object):
             product_id, amount, spoiled, transaction_type
         )
 
+    def add_product_by_barcode(
+        self,
+        barcode: str,
+        amount: float,
+        price: float,
+        best_before_date: datetime = None,
+        get_details: bool = True,
+    ) -> Product:
+        product = Product(
+            self._api_client.add_product_by_barcode(
+                barcode, amount, price, best_before_date
+            )
+        )
+
+        if get_details:
+            product.get_details(self._api_client)
+        return product
+
+    def consume_product_by_barcode(
+        self,
+        barcode: str,
+        amount: float = 1,
+        spoiled: bool = False,
+        get_details: bool = True,
+    ) -> Product:
+        product = Product(
+            self._api_client.consume_product_by_barcode(barcode, amount, spoiled)
+        )
+
+        if get_details:
+            product.get_details(self._api_client)
+        return product
+
     def shopping_list(self, get_details: bool = False) -> List[ShoppingListProduct]:
         raw_shoppinglist = self._api_client.get_shopping_list()
         shopping_list = [ShoppingListProduct(resp) for resp in raw_shoppinglist]
diff --git a/pygrocy/grocy_api_client.py b/pygrocy/grocy_api_client.py
index 1348248..326220c 100644
--- a/pygrocy/grocy_api_client.py
+++ b/pygrocy/grocy_api_client.py
@@ -223,6 +223,19 @@ class MealPlanSectionResponse(BaseModel):
     row_created_timestamp: datetime
 
 
+class StockLogResponse(BaseModel):
+    id: int
+    product_id: int
+    amount: int
+    best_before_date: date
+    purchased_date: date
+    used_date: Optional[date] = None
+    spoiled: bool = False
+    stock_id: str
+    transaction_id: str
+    transaction_type: TransactionType
+
+
 def _enable_debug_mode():
     _LOGGER.setLevel(logging.DEBUG)
 
@@ -335,6 +348,12 @@ class GrocyApiClient(object):
         if parsed_json:
             return ProductDetailsResponse(**parsed_json)
 
+    def get_product_by_barcode(self, barcode) -> ProductDetailsResponse:
+        url = f"stock/products/by-barcode/{barcode}"
+        parsed_json = self._do_get_request(url)
+        if parsed_json:
+            return ProductDetailsResponse(**parsed_json)
+
     def get_chores(self) -> List[CurrentChoreResponse]:
         parsed_json = self._do_get_request("chores")
         return [CurrentChoreResponse(**chore) for chore in parsed_json]
@@ -394,6 +413,49 @@ class GrocyApiClient(object):
 
         self._do_post_request(f"stock/products/{product_id}/consume", data)
 
+    def add_product_by_barcode(
+        self,
+        barcode: str,
+        amount: float,
+        price: float,
+        best_before_date: datetime = None,
+    ) -> StockLogResponse:
+        data = {
+            "amount": amount,
+            "transaction_type": TransactionType.PURCHASE.value,
+            "price": price,
+        }
+
+        if best_before_date is not None:
+            data["best_before_date"] = localize_datetime(best_before_date).strftime(
+                "%Y-%m-%d"
+            )
+
+        parsed_json = self._do_post_request(
+            f"stock/products/by-barcode/{barcode}/add", data
+        )
+
+        if parsed_json:
+            stockLog = [StockLogResponse(**response) for response in parsed_json]
+            return stockLog[0]
+
+    def consume_product_by_barcode(
+        self, barcode: str, amount: float = 1, spoiled: bool = False
+    ):
+        data = {
+            "amount": amount,
+            "spoiled": spoiled,
+            "transaction_type": TransactionType.CONSUME.value,
+        }
+
+        parsed_json = self._do_post_request(
+            f"stock/products/by-barcode/{barcode}/consume", data
+        )
+
+        if parsed_json:
+            stockLog = [StockLogResponse(**response) for response in parsed_json]
+            return stockLog[0]
+
     def get_shopping_list(self) -> List[ShoppingListItem]:
         parsed_json = self._do_get_request("objects/shopping_list")
         return [ShoppingListItem(**response) for response in parsed_json]


Test patch:
diff --git a/test/cassettes/test_grocy/TestGrocy.test_add_product_by_barcode_error.yaml b/test/cassettes/test_grocy/TestGrocy.test_add_product_by_barcode_error.yaml
new file mode 100644
index 0000000..b69559e
--- /dev/null
+++ b/test/cassettes/test_grocy/TestGrocy.test_add_product_by_barcode_error.yaml
@@ -0,0 +1,44 @@
+interactions:
+- request:
+    body: '{"amount": 1, "transaction_type": "purchase", "price": 5}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '57'
+      Content-Type:
+      - application/json
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: POST
+    uri: https://localhost/api/stock/products/by-barcode/555/add
+  response:
+    body:
+      string: '{"error_message":"No product with barcode 555 found"}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Tue, 23 Nov 2021 09:41:06 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 400
+      message: Bad Request
+version: 1
diff --git a/test/cassettes/test_grocy/TestGrocy.test_add_product_by_barcode_valid.yaml b/test/cassettes/test_grocy/TestGrocy.test_add_product_by_barcode_valid.yaml
new file mode 100644
index 0000000..ce06372
--- /dev/null
+++ b/test/cassettes/test_grocy/TestGrocy.test_add_product_by_barcode_valid.yaml
@@ -0,0 +1,105 @@
+interactions:
+- request:
+    body: '{"amount": 1, "transaction_type": "purchase", "price": 5}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '57'
+      Content-Type:
+      - application/json
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: POST
+    uri: https://localhost/api/stock/products/by-barcode/42141099/add
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAEA21Q207EIBD9FcPzblJwrdpfMYbQYdYltgwZIMYY/12mxN2a+Macw8y5vHyp4NWk
+        9Emrg0pMvkKxG3RqgFupxiJ8G2bMxc54JkbrXcEGm8Hoo9ZHcy/bleHiMvp/2XojYl2Wg8qJwoIi
+        PrTlXAjeu/Con2EecESHMDaqsIvZQQkUbflMovsrJaocQKCH9q7RU5RBLvbBlrA2325Nauq6lDBe
+        PXaI6cMCY8vk9/938e60mYyZ9FM7vBC4zcy1JkYICTf3/SAQMy63Xx3dB9l2JamHs9PuEeRyL0Hc
+        CN2X8oVSCvHN/pHtXOuUe2dafb/+AANkFP3MAQAA
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Tue, 23 Nov 2021 12:22:18 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/stock/products/4
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAEA81Vy27bMBD8lYDnOLAUG6l97Q/0XhQLmlpJRCiR4cOuE+Tfu0tKttMGhnvLxbD2
+        Re7M7PJNOG+bpKLYvgndiK1YiXsxygHp73evgwv03WBQXruo7Si2YzLmfk6DztvkIGdWFClV1HvO
+        5Q9jleSc4ubCobfO6bGDD65S8SVRHLjkVS8Dl3ikhGIM0arns6WlU6w/hUK0MEdUD0vKGvRYLCAH
+        m0ZqTlRsb7CVyUTYYeCf1nqERh4D+a+5QbYRPViH1P4tka1HfKU2b4uOvTycg51WMdG9Wm0QChEF
+        HxzljkxRkvOAuusj9HJszDn1wsUnZyhGG0H1qJ4nQNpkqLIZcIxA/YNHpR3OCDgqTo5JE5m3criS
+        xnqd46qaiVFpSEZGhL+xBttCSLu5xly5SXT1o5ul8ZI0XUnZMaQBL1jKd+51g0CyyaSC3aPfazxM
+        YHp7AOWRjm4g6oGYlIMjX72sq0VVLer6rnrc1sttXV8w7rymvkpBI3dopmrSGKqXLeCI4zRqVgur
+        gbSaTAMM4I7o8PZ1ov/9rP6d9MrSdIjtz2l8GJtL+PI8TWFUeFVXq2q52VBUlvY8TrNOC9rXxsRI
+        0i61owjKEv1/iFA/U+b7LxrRXG2aOR7/E4zcSPamgP84CpDzpcWa+ym2vTSJSa6/VQ/rk7VE5gnK
+        xTK+PNQT9SC7zmPHpFLuepNlcHnIlPoxrih8HuqXJMeo4zFzeFoOp73G/Ux77YekbfLpVrsZyoo7
+        5nLgTPKS5cRVA1mLgYdrIF0wRSSYj5fLnX2Rm13qSTBjct/N+hKrh6cnMllDGj+pbrbmzGtSnZm5
+        FjPi7wi8HBoiP+uv2iyWNMkM8PxKnKC6fJxo9x3vVHI7K31DwZ88UjfTWfNzpQNNOS1u9HSPpSDW
+        JK0e2SGEHk0LRrfzc7F4rEnwzmoDnpcgrQ5Fi1Nsn9a5zlnO593GmqeXjbaxNg0po5Um4PsfAAPk
+        yIEHAAA=
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Tue, 23 Nov 2021 12:22:18 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_grocy/TestGrocy.test_consume_product_by_barcode_valid.yaml b/test/cassettes/test_grocy/TestGrocy.test_consume_product_by_barcode_valid.yaml
new file mode 100644
index 0000000..2b0f9ec
--- /dev/null
+++ b/test/cassettes/test_grocy/TestGrocy.test_consume_product_by_barcode_valid.yaml
@@ -0,0 +1,105 @@
+interactions:
+- request:
+    body: '{"amount": 1, "spoiled": false, "transaction_type": "consume"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '62'
+      Content-Type:
+      - application/json
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: POST
+    uri: https://localhost/api/stock/products/by-barcode/42141099/consume
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAEA3VQy07EMAz8FZTzFtWhLLS/glCUJl42oo2jPIQQ4t9xErG7HDh6xh7PzMuXcFYs
+        AiYpDiJEssVk1aCJAb1T8Zn5AXhaMWW14okiKqszMi5HmIcRBpjreYnmrBPaKyuZgkE+MFv+I1Ig
+        t2F1MfJaymTeu4MjzMYeR1gnsNVejtonbbIjr/JnqP8N+VR2rM+jMxWB+7l6Kd6Sr3PV7IPKbucE
+        eg9i8WXbDoIC+ovbDkX6UCYip7O3+xz0N8odyEXKBZ5ZeCOjm51LYxGNC9j8d0FDMeJ23erobZR2
+        27Kak36cYH5i5V5DdVPpfpTOFILzb+rP285xu7G3BuL79QeC7pDY1wEAAA==
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Tue, 23 Nov 2021 12:22:18 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/stock/products/4
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAEA81V227bMAz9lULPTRG7ydbkdT+w92EgFJm2hcqWqkuytOi/j5StJN2KIHvbS9Hw
+        ZvKcQ+pNOG+bpKLYvgndiK1YiXsxygHp329eBxfod4NBee2itqPYjsmY+5IGnbfJQc6sKFKqqPec
+        yz+MVZJzJjcXDr11To8dfHBNFV8SxYFLXvUycIlHSpiMIVr1fLa09BXrT6EQLZSI6mFJWYMeJwvI
+        waaRhhMV2xtsZTIRdhj4T2s9QiOPgfzX3CDbiB6sQxr/lsjWI77SmLdFx14ezsFOq5ior1YbhImI
+        CR8c5Y5MUZLzgLrrI/RybMw59cLFX85QjDaC6lE9z4C0yVBlM+AYgeYHj0o7LAg4Kk6OWROZt+nj
+        ShrrdY6raiZGpSEZGRH+xBpsCyHtSo1SuUnU+tEVabwkTS0pO4Y04AVLuedeNwgkm0wq2D36vcbD
+        DKa3B1Ae6dMNRD0Qk3Jw5KuXdbWoqkVd31WP23q5resLxp3XNNdU0MgdmrmaNIbqZQs44jiNmtXC
+        aiCtJtMAA7gjOrx9nel/P6t/J72ytB1i+2NeH8bmEr68T3MYFV7V1apabjYUlaVd1qnodEL72poY
+        SdqlcRRBOUX/GyI0z5z5/pNWNFebd47X/wQjD5K9KeBfjgnI0rRYPzFevKSwlyYxyfXXzcOaj8Bl
+        aF6hXC0DnBOmIiC7zmPHrFLy+inr4JPUj3GTxMtWvyQ5Rh2PmcTTdTgdNh5oPmzfJZ2TT8/azVhW
+        TCGXA2eSl6wnrhrIOhl4uwYSBnNEivnYXJ7sP+nsUlBiTf3LfVcEJlYPT7xG1pDIT7Ir1px5TauF
+        mWsxI/6KwNehIfKzAKvNYkmrzACXZ+IE1eXrRMfveKeS21npGwr+5JW6mc6apaoDrTldbvTUx1IQ
+        a5Juj+wQQo+mBaPb8l4sHmuStrPagOcrSLdD0eUU2y/LXOcs5/NxY83T00bnWJuGlNFKE/D9N9N4
+        xRmCBwAA
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Tue, 23 Nov 2021 12:22:18 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_grocy/TestGrocy.test_consume_product_error.yaml b/test/cassettes/test_grocy/TestGrocy.test_consume_product_error.yaml
new file mode 100644
index 0000000..6483b29
--- /dev/null
+++ b/test/cassettes/test_grocy/TestGrocy.test_consume_product_error.yaml
@@ -0,0 +1,44 @@
+interactions:
+- request:
+    body: '{"amount": 1, "spoiled": 5, "transaction_type": "consume"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '58'
+      Content-Type:
+      - application/json
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: POST
+    uri: https://localhost/api/stock/products/by-barcode/555/consume
+  response:
+    body:
+      string: '{"error_message":"No product with barcode 555 found"}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Tue, 23 Nov 2021 09:41:06 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 400
+      message: Bad Request
+version: 1
diff --git a/test/cassettes/test_product/TestProduct.test_get_product_by_barcode.yaml b/test/cassettes/test_product/TestProduct.test_get_product_by_barcode.yaml
new file mode 100644
index 0000000..a32d4b1
--- /dev/null
+++ b/test/cassettes/test_product/TestProduct.test_get_product_by_barcode.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/stock/products/by-barcode/42141099
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAEA81Vy27bMBD8lUDnOLDkGEV87Q/0XhQLmqIkIpRIU6RdJ8i/d5aUbKUwAvfWi2Ht
+        i9yZ2eV74bytowzF7r3QdbErnovHYhC9wt/vXo9uxHetRum1C9oOxW6IxjzOadR6Gx2lzBKRQgZ9
+        5Fz+MFYKzsluLjx21jk9tPTJlSseIuLIRS87MXKJDRKycQxWvl4tDU6x/hJKwdIcUT6tkdXrIVtI
+        9DYOaK4o2V6rRkQTaK9G/mmsV1SL8wj/V24STVCerFNo/57Ixiv1hjbviw6dOF2DnZYh4l6NNooy
+        ERkfNYg9TEHAeVK67QJ1YqjNNXXh4pMTFIMNJDslXydAmmhQ2fRqCIT+ySupnZoRcCgOx6SJxFs+
+        XApjvU5xZcXEyNhHI4Kiv7Em29AY93ONuXIdcfWzm6VxiBpXknYYY68WLKU7d7pWBNkkUskelT9q
+        dZrA9PZE0iscXVPQPZgUvYOvWlflqixXVfVQbnbVeldVC8ad1+grFzRir8xUTRiDeslCDhzHQbNa
+        WA3QajQ1MYB70OHt20T/x1X9e+GlxXQUu5/T+DA2S/jSPE1hKPxclc/l+uUFUUna8zjNOs1ofzUm
+        RkC7aEcCyhz9b4ignynz4xdGNFWbZo7Hf4axZPSSN44KjuliPIhXurYMUzIdhYmJ229PW87M1txV
+        GhyukWFdlCDRtl61zCW820T+jczPYVnX8ygfohiCDufE3GUlXLYZ0zFtsx8CO+TmLrsbwJJ543Lk
+        TPSCRcRVR1izgUeqhxoYLcjk8+VSZ//JzZYqKjZP1YapFMd2VhZsbLEG4r7IbTKm3NsSLZYzdzsk
+        C2lQvwPxTqhBfpZdtVpvV9Xy3bhgtXyTsPLODzK6vRW+xhVvvE1385lO0yOGG/ta+aRQ0CawcUSr
+        aOyUacjoZn4lqnINaTurDXnefdgYEvuy2MGMMlc1X0eENwneM+xgbWoooxFmVB9/AA9z8G53BwAA
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Mon, 22 Nov 2021 13:29:27 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/test/cassettes/test_product/TestProduct.test_product_by_barcode_get_details_non_existant.yaml b/test/cassettes/test_product/TestProduct.test_product_by_barcode_get_details_non_existant.yaml
new file mode 100644
index 0000000..a8fc62d
--- /dev/null
+++ b/test/cassettes/test_product/TestProduct.test_product_by_barcode_get_details_non_existant.yaml
@@ -0,0 +1,40 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      User-Agent:
+      - python-requests/2.26.0
+      accept:
+      - application/json
+    method: GET
+    uri: https://localhost/api/stock/products/by-barcode/200
+  response:
+    body:
+      string: '{"error_message":"No product with barcode 200 found"}'
+    headers:
+      Access-Control-Allow-Headers:
+      - '*'
+      Access-Control-Allow-Methods:
+      - GET, POST, PUT, DELETE, OPTIONS
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Type:
+      - application/json
+      Date:
+      - Mon, 22 Nov 2021 13:29:27 GMT
+      Server:
+      - nginx/1.20.1
+      Transfer-Encoding:
+      - chunked
+      X-Powered-By:
+      - PHP/8.0.11
+    status:
+      code: 400
+      message: Bad Request
+version: 1
diff --git a/test/test_grocy.py b/test/test_grocy.py
index d1bec24..bb63f0a 100644
--- a/test/test_grocy.py
+++ b/test/test_grocy.py
@@ -1,5 +1,8 @@
 import json
 from datetime import datetime
+
+import pytest
+
 from test.test_const import CONST_BASE_URL, CONST_PORT, CONST_SSL
 from unittest import TestCase
 from unittest.mock import mock_open, patch
@@ -9,6 +12,7 @@ import responses
 from pygrocy import Grocy
 from pygrocy.errors import GrocyError
 from pygrocy.grocy_api_client import GrocyApiClient
+from pygrocy.data_models.product import Product
 
 
 class TestGrocy(TestCase):
@@ -216,6 +220,40 @@ class TestGrocy(TestCase):
         )
         self.assertRaises(GrocyError, self.grocy.consume_product, 1, 1.3)
 
+    @pytest.mark.vcr
+    def test_add_product_by_barcode_valid(self):
+        product = self.grocy.add_product_by_barcode(
+            "42141099", 1, 5, self.date_test, True
+        )
+
+        assert isinstance(product, Product)
+        assert product.id == 4
+        assert product.name == "Crisps"
+
+    @pytest.mark.vcr
+    def test_add_product_by_barcode_error(self):
+        with pytest.raises(GrocyError) as exc_info:
+            self.grocy.add_product_by_barcode("555", 1, 5, self.date_test, True)
+
+        error = exc_info.value
+        assert error.status_code == 400
+
+    @pytest.mark.vcr
+    def test_consume_product_by_barcode_valid(self):
+        product = self.grocy.consume_product_by_barcode("42141099", 1, False, True)
+
+        assert isinstance(product, Product)
+        assert product.id == 4
+        assert product.name == "Crisps"
+
+    @pytest.mark.vcr
+    def test_consume_product_error(self):
+        with pytest.raises(GrocyError) as exc_info:
+            self.grocy.consume_product_by_barcode("555", 1, False, True)
+
+        error = exc_info.value
+        assert error.status_code == 400
+
     @responses.activate
     def test_execute_chore_valid(self):
         responses.add(responses.POST, f"{self.base_url}/chores/1/execute", status=200)
diff --git a/test/test_product.py b/test/test_product.py
index 44c52fc..1fcef03 100644
--- a/test/test_product.py
+++ b/test/test_product.py
@@ -56,3 +56,26 @@ class TestProduct:
         mocker.patch("builtins.open", mocker.mock_open())
 
         assert grocy.add_product_pic(20, "/somepath/pic.jpg") is None
+
+    @pytest.mark.vcr
+    def test_get_product_by_barcode(self, grocy):
+        product = grocy.product_by_barcode("42141099")
+
+        assert isinstance(product, Product)
+        assert product.name == "Crisps"
+        assert product.available_amount == 5
+        assert product.product_group_id == 1
+
+        assert len(product.product_barcodes) == 1
+        barcode = product.product_barcodes[0]
+        assert isinstance(barcode, ProductBarcode)
+        assert barcode.barcode == "42141099"
+
+    @pytest.mark.vcr
+    def test_product_by_barcode_get_details_non_existant(self, grocy):
+        with pytest.raises(GrocyError) as exc_info:
+            grocy.product_by_barcode(200)
+
+        error = exc_info.value
+        assert error.status_code == 400
+        assert error.message == "No product with barcode 200 found"