Repository: SebRut/pygrocy (python)

Issue:
Missing data in CurrentVolatilStockResponse._missing_products
Hi!

First of all many thanks for pygrocy, I have been using it for a while and it helped me a lot!

I think I got a parse error when I try to get the missing products using `grocy.missing_products()`

If I check the JSON I See:

```
"missing_products": [
        {
            "id": "7",
            "name": "XXXX",
            "amount_missing": "1",
            "is_partly_in_stock": "1"
        },
        {
            "id": "8",
            "name": "YYYY",
            "amount_missing": "1",
            "is_partly_in_stock": "1"
        },
        {
            "id": "75",
            "name": "ZZZZ",
            "amount_missing": "1",
            "is_partly_in_stock": "1"
        }
    ]
```

But diving in [CurrentStockResponse](https://github.com/SebRut/pygrocy/blob/47dbb07f93403500f133a13012264aa1439e68db/pygrocy/grocy_api_client.py#L177) is expecting antother property names: 

```
self._product_id = parse_int(parsed_json.get('product_id'))
self._amount = parse_float(parsed_json.get('amount'))
self._best_before_date = parse_date(parsed_json.get('best_before_date'))
self._amount_opened = parse_float(parsed_json.get('amount_opened'))
```

Do we might need a new class to represent missing products?

If so, I can try to open a PR with that change.

Pull request:
Add Missing Products Class
## Description

Added new class for missing products.
Changed Products constructor to pass in generic object to be parsed.
Added unit test

**Related issue (if applicable):** fixes #62 

## 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/README.md b/README.md
index 1fe7a78..d563fbf 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ for entry in grocy.stock():
     print("{} in stock for product id {}".format(entry.product_id, entry.amount))
 ```
 
-## Dev testing
+## Development testing
 You need a Grocy instance running in demo mode at localhost with https (docker or a php server)
 You can setup url, port and ssl in test/test_const.py
 Easy case : install docker and docker-compose and run :
diff --git a/pygrocy/grocy.py b/pygrocy/grocy.py
index ace4e7f..0178df1 100644
--- a/pygrocy/grocy.py
+++ b/pygrocy/grocy.py
@@ -1,22 +1,30 @@
 from datetime import datetime
 from typing import List
 
-from .grocy_api_client import (ChoreDetailsResponse, CurrentChoreResponse,
-                               CurrentStockResponse,
-                               ShoppingListItem,
-                               LocationData,
+from .grocy_api_client import (DEFAULT_PORT_NUMBER, ChoreDetailsResponse,
+                               CurrentChoreResponse, CurrentStockResponse,
                                CurrentVolatilStockResponse, GrocyApiClient,
+                               LocationData, MissingProductResponse,
                                ProductData, ProductDetailsResponse,
-                               TransactionType, UserDto, DEFAULT_PORT_NUMBER)
+                               ShoppingListItem, TransactionType, UserDto)
 
 
 class Product(object):
-    def __init__(self, stock_response: CurrentStockResponse):
-        self._product_id = stock_response.product_id
-        self._available_amount = stock_response.amount
-        self._best_before_date = stock_response.best_before_date
-
-        self._name = None
+    def __init__(self, response):
+        if isinstance(response, CurrentStockResponse):
+            self._product_id = response.product_id
+            self._name = None
+            self._available_amount = response.amount
+            self._best_before_date = response.best_before_date
+            self._amount_missing = None
+            self._is_partly_in_stock = None
+        elif isinstance(response, MissingProductResponse):
+            self._product_id = response.product_id
+            self._name = response.name
+            self._available_amount = None
+            self._best_before_date = None
+            self._amount_missing = response.amount_missing
+            self._is_partly_in_stock = response.is_partly_in_stock
         self._barcodes = None
         self._product_group_id = None
 
@@ -52,6 +60,14 @@ class Product(object):
     def barcodes(self) -> List[str]:
         return self._barcodes
 
+    @property
+    def amount_missing(self) -> float:
+        return self._amount_missing
+
+    @property
+    def is_partly_in_stock(self) -> int:
+        return self._is_partly_in_stock
+
 class Group(object):
     def __init__(self, raw_product_group: LocationData):
         self._id = raw_product_group.id
@@ -103,7 +119,7 @@ class ShoppingListProduct(object):
         if self._product_id is None:
             self.get_details()
         return self._product
-    
+
 class Chore(object):
     def __init__(self, raw_chore: CurrentChoreResponse):
         self._chore_id = raw_chore.chore_id
@@ -261,4 +277,3 @@ class Grocy(object):
         
     def get_last_db_changed(self):
         return self._api_client.get_last_db_changed()
-        
\ No newline at end of file
diff --git a/pygrocy/grocy_api_client.py b/pygrocy/grocy_api_client.py
index 613ee0f..0388422 100644
--- a/pygrocy/grocy_api_client.py
+++ b/pygrocy/grocy_api_client.py
@@ -1,15 +1,15 @@
+import base64
+import json
+import os
 from datetime import datetime
 from enum import Enum
 from typing import List
-from tzlocal import get_localzone
 from urllib.parse import urljoin
-import os
-import base64
-import json
+
 import pytz
 import requests
-
 from pygrocy.utils import parse_date, parse_float, parse_int
+from tzlocal import get_localzone
 
 DEFAULT_PORT_NUMBER=9192
 
@@ -196,11 +196,34 @@ class CurrentStockResponse(object):
     def amount_opened(self) -> float:
         return self._amount_opened
 
+class MissingProductResponse(object):
+    def __init__(self, parsed_json):
+        self._product_id = parse_int(parsed_json.get('id'))
+        self._name = parsed_json.get('name')
+        self._amount_missing = parse_float(parsed_json.get('amount_missing'))
+        self._is_partly_in_stock = bool(parse_int(parsed_json.get('is_partly_in_stock')))
+
+    @property
+    def product_id(self) -> int:
+        return self._product_id
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def amount_missing(self) -> float:
+        return self._amount_missing
+
+    @property
+    def is_partly_in_stock(self) -> bool:
+        return self._is_partly_in_stock
+
 class CurrentVolatilStockResponse(object):
     def __init__(self, parsed_json):
         self._expiring_products = [CurrentStockResponse(product) for product in parsed_json.get('expiring_products')]
         self._expired_products = [CurrentStockResponse(product) for product in parsed_json.get('expired_products')]
-        self._missing_products = [CurrentStockResponse(product) for product in parsed_json.get('missing_products')]
+        self._missing_products = [MissingProductResponse(product) for product in parsed_json.get('missing_products')]
 
     @property
     def expiring_products(self) -> List[CurrentStockResponse]:
@@ -211,7 +234,7 @@ class CurrentVolatilStockResponse(object):
         return self._expired_products
 
     @property
-    def missing_products(self) -> List[CurrentStockResponse]:
+    def missing_products(self) -> List[MissingProductResponse]:
         return self._missing_products
 
 


Test patch:
diff --git a/test/test_missingProductResponse.py b/test/test_missingProductResponse.py
new file mode 100644
index 0000000..345009a
--- /dev/null
+++ b/test/test_missingProductResponse.py
@@ -0,0 +1,24 @@
+import json
+from unittest import TestCase
+
+from pygrocy.grocy_api_client import MissingProductResponse
+
+
+class TestMissingProductResponse(TestCase):
+    def test_parse_partly_in_stock_true(self):
+        input_json = """{ "id": "7", "name": "XXXX", "amount_missing": "2", "is_partly_in_stock": "1" }"""
+        response = MissingProductResponse(json.loads(input_json))
+
+        assert response.product_id == 7
+        assert response.name == "XXXX"
+        assert response.amount_missing == 2
+        assert response.is_partly_in_stock
+
+    def test_parse_partly_in_stock_false(self):
+        input_json = """{ "id": "7", "name": "XXXX", "amount_missing": "2", "is_partly_in_stock": "0" }"""
+        response = MissingProductResponse(json.loads(input_json))
+
+        assert response.product_id == 7
+        assert response.name == "XXXX"
+        assert response.amount_missing == 2
+        assert not response.is_partly_in_stock