# implements synchronization between plentymarkets and shopware

import csv
import urllib

from datetime import datetime
from itertools import zip_longest, chain
from logging import debug, error, warning, info

# pypi imports
import pytz
from bs4 import BeautifulSoup

# own imports
from ..base import *
from ..config import *
from ..lib.plentyrest import PlentyAPI, PlentyItem, PlentyVariation,\
    PlentyOrder, PlentyAddress, PlentyOrderItem, PlentyAmazonPayPayment,\
    PlentyManualPayment
from ..lib.shopwarerest import ShopwareProduct, ShopwareOrder,\
    ShopwareOrderList
from ..lib.plentygmz import GMZCheapestPriceHistory


class PlentySyncException(Exception):
    pass


class PlentyToShopwareItemSyncSimple:
    """Synchronisation handler between a plenty item and shopware product.
     Simplified version, that disregards variations."""

    VATIDMAP = {
        0: '018eeb96f3567336a3b9db72ca7d7b31',
        1: '018eeb96f3567336a3b9db72cae92d34'
    }

    def __init__(self, plentyitem, shopwareproduct, plenty, sws):
        self.pitem = plentyitem
        self.pvar = plentyitem.main_variation
        self.swprod = shopwareproduct
        self.plenty = plenty
        self.sws = sws

    def get_all_shopware_channel_prices(self):
        """Get all prices on a plenty variation set for shopware channels"""

        prices = set()
        for channel, priceid in CHANNELTOPRICEMAP.items():
            price = self.pvar.get_sales_price_by_id(priceid)
            if price:
                prices.add((channel, price))
        return prices

    def get_all_active_channels(self):
        """Get all channels set to active on a plenty variation"""

        pchannels = set()
        for channel, marketid in CHANNELTOREFERRERMAP.items():
            if self.pvar.get_market_active_by_id(marketid):
                pchannels.add(channel)
        return pchannels

    def update_active(self):
        # auto deactivate items without image
        active = self.pvar['isActive']
        if len(self.pitem.get_images_for_market(39)) == 0:
            active = False
        self.swprod.set_active(active)

    def diff_active(self):
        pactive = self.pvar['isActive']
        # auto deactivate items without image
        if len(self.pitem.get_images_for_market(39)) == 0:
            pactive = False
        sactive = self.swprod.attr['active']
        if pactive != sactive:
            return ('active', f"{pactive}!={sactive}", self.update_active)

    def update_price(self):
        prices = self.get_all_shopware_channel_prices()
        if len(prices) > 1:
            warning(f"Variation {self.pvar['id']}: channel specific prices "
                    f"not implemented yet, defaulting to channel "
                    f"0193b02a21e57bfd976b741ff6f30207")
            prices = set((p for p in prices
                          if p[0] == '0193b02a21e57bfd976b741ff6f30207'))
        if not prices:
            raise PlentySyncException(
                f"Variation {self.pvar['id']}: no shopware price defined")
        newprice = prices.pop()[1]
        # save price change in plenty property
        h = GMZCheapestPriceHistory.get_from_variation(self.pvar)
        h.add_price(newprice)
        h.save_to_plenty(self.plenty, self.pvar['id'])
        # UVP
        listprice = self.pvar.get_sales_price_by_id(15)
        # update regular, list and regulation price
        self.swprod.set_gross_price(newprice, listprice, h.get_cheapest())

    def diff_price(self):
        prices = self.get_all_shopware_channel_prices()
        if len(prices) > 1:
            warning(f"Variation {self.pvar['id']}: channel specific prices "
                    f"not implemented yet, defaulting to channel "
                    f"0193b02a21e57bfd976b741ff6f30207")
            prices = set((p for p in prices
                          if p[0] == '0193b02a21e57bfd976b741ff6f30207'))
        if prices:
            # pick any price as
            pprice = prices.pop()[1]
        else:
            pprice = None
        sprice = self.swprod.gross_price
        if sprice != pprice:
            return ('price', f"{pprice}!={sprice}", self.update_price)
        slistprice = self.swprod.list_price
        plistprice = self.pvar.get_sales_price_by_id(15)
        if slistprice != plistprice:
            return ('listprice', f"{plistprice}!={slistprice}",
                    self.update_price)

    def update_taxid(self):
        taxid = self.VATIDMAP[self.pvar['vatId']]
        self.swprod.set_taxid(taxid)

    def diff_taxid(self):
        staxid = self.swprod.attr['taxId']
        ptaxid = self.VATIDMAP[self.pvar['vatId']]
        if staxid != ptaxid:
            return ('taxid', f"{ptaxid}!={staxid}", self.update_taxid)

    def update_channels(self):
        pchannels = self.get_all_active_channels()
        if (len(pchannels) > 1 and
                len(self.get_all_shopware_channel_prices()) > 1):
            raise PlentySyncException(
                f"Variation {self.pvar['id']}: channel specific prices not "
                f"implemented yet")
        self.swprod.set_channels(pchannels)

    def diff_channel(self):
        schannels = self.swprod.active_channels
        pchannels = set()
        for channel, marketid in CHANNELTOREFERRERMAP.items():
            if self.pvar.get_market_active_by_id(marketid):
                pchannels.add(channel)
        if schannels != pchannels:
            return ('channels', f"{pchannels}!={schannels}",
                    self.update_channels)

    def update_closeout(self):
        self.swprod.set_closeout(True)

    def diff_closeout(self):
        """closeout 'Abverkauf' should always be active"""

        if not self.swprod.is_closeout:
            return ('closeout', f"False!=True", self.update_closeout)

    def update_ean(self):
        self.swprod.set_ean(self.pvar.ean)

    def diff_ean(self):
        sean = self.swprod.ean
        pean = self.pvar.ean
        if sean != pean:
            return ('ean', f"{pean}!={sean}", self.update_ean)

    def update_releasedate(self):
        preldate = self.pvar.releasedate
        if preldate:
            preldate = preldate[:19]
        self.swprod.set_releasedate(preldate)

    def diff_releasedate(self):
        # only compare date and time part without timezone
        sreldate = self.swprod.releasedate
        if sreldate:
            sreldate = sreldate[:19]
        preldate = self.pvar.releasedate
        if preldate:
            preldate = preldate[:19]
        if sreldate != preldate:
            return ('releasedate', f"{preldate}!={sreldate}",
                    self.update_releasedate)

    def update_dimensions(self):
        self.swprod.set_dimensions(self.pvar.dimensions)

    def diff_dimensions(self):
        sdim = self.swprod.dimensions
        pdim = self.pvar.dimensions
        if sdim != pdim:
            return ('dimensions', f"{pdim}!={sdim}", self.update_dimensions)

    def update_manufacturer(self):
        pmanname = self.pitem.manufacturer.name
        self.swprod.set_manufacturer_by_name(pmanname)

    def diff_manufacturer(self):
        smanname = self.swprod.manufacturername
        pmanname = self.pitem.manufacturer.name
        if smanname != pmanname:
            return ('manufacturer', f"{pmanname}!={smanname}",
                    self.update_manufacturer)

    def update_eu_responsible_person(self):
        pperson = self.pitem.manufacturer.eu_responsible_person
        self.swprod.set_eu_responsible_person(pperson)

    def diff_eu_responsible_person(self):
        sperson = self.swprod.eu_responsible_person
        pperson = self.pitem.manufacturer.eu_responsible_person
        if sperson != pperson:
            return ('eu_responsible_person', f"{pperson}!={sperson}",
                    self.update_eu_responsible_person)

    def update_name_from_property(self):
        pvarname = self.pvar.get_property(274).value
        self.swprod.set_name(pvarname)

    def update_name(self):
        pname = self.pitem.name
        # name override in variation properties
        prop = self.pvar.get_property(274)
        if prop:
            pname = prop.value
        self.swprod.set_name(pname)

    def diff_name(self):
        sname = self.swprod.name
        pname = self.pitem.name
        # name override in variation properties
        prop = self.pvar.get_property(274)
        if prop:
            pvarname = prop.value
        else:
            pvarname = None
        diff = None
        if not pvarname and sname != pname:
            diff = ('name', f"{pname}!={sname}", self.update_name)
        if pvarname and sname != pvarname:
            diff = ('name', f"{pvarname}!={sname}",
                    self.update_name_from_property)
        return diff

    def cleanup_description(self, desc):
        pperson = self.pitem.manufacturer.eu_responsible_person
        soup = BeautifulSoup(desc, features='html.parser')
        for tag in soup():
            del tag.attrs
        extra = ("<h2>Details zur Produktsicherheit</h2><p><strong>"
                 "Verantwortliche Person für die EU:</strong><br />"
                 "Verantwortlich für dieses Produkt ist der in der EU "
                 "ansässige Wirtschaftsakteur:<br /></p><p>")
        return str(soup) + extra + pperson + '</p>'

    def update_description(self):
        sdesc = self.swprod.description
        pdesc = self.pvar.get_property(275)
        if pdesc:
            pdesc = pdesc.value
        else:
            pdesc = self.pitem.description
        # clean up description for shopware
        pdesc = self.cleanup_description(pdesc)
        self.swprod.set_description(pdesc)

    def diff_description(self):
        sdesc = self.swprod.description
        pdesc = self.pvar.get_property(275)
        if pdesc:
            pdesc = pdesc.value
        else:
            pdesc = self.pitem.description
        # clean up description for shopware
        pdesc = self.cleanup_description(pdesc)
        if (BeautifulSoup(str(sdesc), features='html.parser') !=
                BeautifulSoup(str(pdesc), features='html.parser')):
            diff = ('description', f"{str(pdesc)[:30]}..!={str(sdesc)[:30]}..",
                    self.update_description)
            return diff

    def update_images(self):
        pimages = self.pvar.get_images_for_market(39)
        if not pimages:
            warning(f"{self.pvar['id']} has no images linked to variation "
                    f"using images from item")
            pimages = self.pitem.get_images_for_market(39)
        simages = self.swprod.media
        position = 0
        for pi, si in zip_longest(pimages, simages):
            pimd5 = 'missing' if not pi else pi.md5checksum
            simd5 = 'missing' if not si else si.md5checksum
            if si and not pi:
                # if no plenty image delete shopware image
                self.swprod.delete_media_by_id(si.id)
            if (si and pi) and (simd5 != pimd5):
                self.swprod.replace_media_by_id(si.id, position, pi.url,
                                                pi.md5checksum,
                                                f"{self.pvar['id']}_")
            if pi and not si:
                self.swprod.add_media(position, pi.url, pi.md5checksum,
                                      f"{self.pvar['id']}_")
            position += 1

    def diff_images(self):
        pimages = self.pvar.get_images_for_market(39)
        if not pimages:
            pimages = self.pitem.get_images_for_market(39)
        simages = self.swprod.media
        difftexts = []
        for pi, si in zip_longest(pimages, simages):
            pimd5 = 'missing' if not pi else pi.md5checksum
            simd5 = 'missing' if not si else si.md5checksum
            if pimd5 != simd5:
                difftexts.append(f"{pimd5}!={simd5}")
        if difftexts:
            diff = ('images', difftexts, self.update_images)
            return diff

    def update_availability(self):
        self.swprod.set_availability(self.pvar.availability)

    def diff_availability(self):
        pavail = self.pvar.availability
        savail = self.swprod.availability
        if pavail != savail:
            diff = ('availability', f"{pavail}!={savail}",
                    self.update_availability)
            return diff

    def update_categories(self):
        pprop = self.pvar.get_property(278)
        if pprop:
            pcatids = pprop.descriptions
            self.swprod.set_categories(pcatids)

    def diff_categories(self):
        pprop = self.pvar.get_property(278)
        pcatids = pprop.descriptions if pprop else set()
        scatids = self.swprod.categories
        # ignore categries if none defined in plenty
        if pprop and pcatids != scatids:
            diff = ('categories', f"{pcatids}!={scatids}",
                    self.update_categories)
            return diff

    def update_warnings(self):
        pwarn = self.pvar.get_property(150)
        pwarn = '' if not pwarn or not pwarn.name else pwarn.name
        self.swprod.set_warnings(pwarn)

    def diff_warnings(self):
        swarn = self.swprod.warnings
        pwarn = self.pvar.get_property(150)
        pwarn = None if not pwarn else pwarn.name
        if pwarn != swarn:
            diff = ('warnings', f"{pwarn}!={swarn}",
                    self.update_warnings)
            return diff

    def update_features(self):
        pfeat = self.pvar.get_present_property_values(16, 44, 45)
        self.swprod.set_features(pfeat)

    def diff_features(self):
        sfeat = self.swprod.features
        pfeat = self.pvar.get_present_property_values(16, 44, 45)
        if sfeat != pfeat:
            diff = ('features', f"{pfeat}!={sfeat}", self.update_features)
            return diff

    def diff(self):
        diffs = [
            self.diff_active(),
            self.diff_taxid(),
            self.diff_price(),
            self.diff_channel(),
            self.diff_closeout(),
            self.diff_ean(),
            self.diff_releasedate(),
            self.diff_dimensions(),
            self.diff_manufacturer(),
            self.diff_eu_responsible_person(),
            self.diff_name(),
            self.diff_description(),
            self.diff_images(),
            self.diff_availability(),
            self.diff_categories(),
            self.diff_warnings(),
            self.diff_features(),
        ]
        return list(filter(None, diffs))

    def update_diffs(self, diffs):
        for diffname, diffstr, updatefunc in diffs:
            if updatefunc:
                updatefunc()
            else:
                warning(f"Variation {self.pvar['id']}: no update function "
                        f"implemented for '{diffname}' '{diffstr}'")

    def update(self):
        if self.pvar.get_property(279):
            info(f"Ignoring itemid {self.pitem['id']} because of present "
                 "property 279.")
            return
        if not self.get_all_shopware_channel_prices():
            warning(f"Ignoring variation {self.pvar['id']}: no shopware "
                    f"price defined")
            return
        if not self.swprod:
            self.create()
        diffs = self.diff()
        if diffs:
            info(diffs)
        self.update_diffs(diffs)
        self.swprod.save()

    def create(self):
        p = self.sws.create_product_minimal(f"GM{self.pvar['id']:05d}")
        self.swprod = ShopwareProduct.get_product_by_number(
            self.sws, f"GM{self.pvar['id']:05d}", 'all')

    @classmethod
    def by_plenty_itemid(cls, itemid, plenty, sws):
        pitem = PlentyItem.get_by_id(plenty, itemid)
        if pitem.has_variations:
            raise PlentySyncException(
                f"Variation handling not implemented yet.")
        swprod = ShopwareProduct.get_product_by_number(
            sws, f"GM{pitem.main_variationid:05d}", 'all')
        return cls(pitem, swprod, plenty, sws)


class PlentyStockDict(dict):
    """Holds plenty stock data by variation id"""

    @classmethod
    def from_csv_urls(cls, urls):
        d = cls()
        for url in urls:
            with urllib.request.urlopen(url) as f:
                data = f.read().decode('utf-8').split('\n')
                reader = csv.DictReader(data, delimiter=';')
                for row in reader:
                    qty = (int(row['VariationStock.netStock.Halle'] or '0') +
                           int(row['VariationStock.netStock.Vorverkaufslager']
                               or '0'))
                    d[int(row['Variation.id'])] = qty
        return d


class PlentyShopwareItems(set):
    """Set of all plenty items active for shopware export"""

    @classmethod
    def from_csv_urls(cls, urls):
        s = cls()
        for url in urls:
            with urllib.request.urlopen(url) as f:
                data = f.read().decode('utf-8').split('\n')
                reader = csv.DictReader(data, delimiter=';')
                for row in reader:
                    s.add(int(row['Item.id']))
        return s


class ShopwareStockDict(dict):
    """Holds shopware stock data by variation id"""

    @classmethod
    def from_shopware(cls, sws):
        data = sws.list_stock()
        d = cls()
        d.swuuid = {}
        for e in data:
            try:
                vid = int(e['attributes']['productNumber'][2:])
                d[vid] = e['attributes']['stock']
                d.swuuid[vid] = e['id']
            except ValueError:
                warning(f"product {e['attributes']['productNumber']} ignored. "
                        "Could not map to variation id")
        return d


@withshopware
def cmd_syncstock(args, plenty=None, sws=None):
    plentystock = PlentyStockDict.from_csv_urls(PLENTYSTOCKCSVURLS)
    shopwarestock = ShopwareStockDict.from_shopware(sws)
    newstock = {}
    for v, s in shopwarestock.items():
        try:
            if s != plentystock[v]:
                newstock[v] = plentystock[v]
        except KeyError:
            warning(f"No variation {v} found in plenty. Ignoring.")
    # TODO better use bulk update here
    for v, s in newstock.items():
        sws.patch_product(shopwarestock.swuuid[v], {'stock': s})


class GomazingOrder(PlentyOrder):

    DEFAULTS = {
        'typeId': 1,
        'ownerId': 2,
        'referrerId': 9,
        'plentyId': 17603,
        'locationId': 6,
        'statusId': 3,
        'orderItems': [],
        'properties': [
            # payment method prepayment
            {'typeId': 3, 'value': '6000'},
        ],
        'dates': [],
        'addressRelations': [],
        'relations': []
    }

    @classmethod
    def plenty_vid_from_shopware_itemno(cls, prodno, plenty):
        vid = int(prodno[2:])
        r = plenty.get_variation_by_id(
            vid,
            withparam="item,variationBarcodes,variationMarkets")
        return r.json()

    @staticmethod
    def plenty_address_from_shopware_address(swaddr):
        sa = swaddr['attributes']
        pa = PlentyAddress(country=sa['countryiso'])
        pa['name1'] = sa['company']
        pa['name2'] = sa['firstName']
        pa['name3'] = sa['lastName']
        streetandno = sa['street'].rsplit(maxsplit=1)
        pa['address1'] = streetandno[0]
        if len(streetandno) > 1:
            pa['address2'] = streetandno[1]
        if sa['additionalAddressLine1']:
            pa['address3'] = sa['additionalAddressLine1']
        if sa['additionalAddressLine2']:
            pa['address3'] += sa['additionalAddressLine2']
        pa['postalCode'] = sa['zipcode']
        pa['town'] = sa['city']
        return pa

    @classmethod
    def verify_variationid_match(cls, plentyvar, swproduct):
        swean = swproduct['ean']
        plentyeans = [b['code'] for b in plentyvar['variationBarcodes']]
        if swean not in plentyeans:
            prodno = swproduct['productNumber']
            vid = plentyvar['id']
            error(f"GTIN {swean} of {prodno} not found in plenty variation "
                  f"{vid}")
            raise PlentySyncException()

    @classmethod
    def get_plenty_referrer(cls, so):
        channel = so['data']['attributes']['salesChannelId']
        return CHANNELTOREFERRERMAP[channel]

    @classmethod
    def get_plenty_id(cls, so):
        channel = so['data']['attributes']['salesChannelId']
        return CHANNELTOPLENTYIDMAP[channel]

    @classmethod
    def plenty_orderitem_from_shopware_lineitem(cls, swline, refid, sws,
                                                plenty):
        swattr = swline['attributes']
        poi = PlentyOrderItem()
        poi['referrerId'] = refid
        poi['quantity'] = swattr['quantity']
        poi['amounts'][0]['priceOriginalGross'] = swattr['unitPrice']
        if swattr['price']['calculatedTaxes']:
            poi['vatRate'] = swattr['price']['calculatedTaxes'][0]['taxRate']
        else:
            # not taxes
            poi['vatRate'] = 0
        if 'productNumber' in swattr['payload']:
            prodno = swattr['payload']['productNumber']
            swproduct = sws.get_product(
                swattr['productId'])['data']['attributes']
            json = cls.plenty_vid_from_shopware_itemno(prodno, plenty)
            if len(json['entries']) > 1:
                error(f'{prodno} is not a unique variation number')
            elif len(json['entries']) == 1:
                if not prodno.startswith('WM'):
                    cls.verify_variationid_match(json['entries'][0], swproduct)
                poi['itemVariationId'] = json['entries'][0]['id']
                if json['entries'][0]['item']['stockType'] == 3:
                    poi.ispreorder = True
            else:
                error(f'{prodno} could not be found in plenty')
        elif swattr['totalPrice'] < 0:
            # probably a voucher
            poi['itemVariationId'] = 1334
            label = swattr['label']
            poi['orderItemName'] = f'GUTSCHEINCODE "{label}" eingelöst'
        elif swattr['totalPrice'] == 0 and not swattr['productId']:
            info(f'no product and no recognized discount')
            return None
        else:
            error(f'no productNumber and price positive')
        return poi

    @classmethod
    def from_shopware_order(cls, so, plenty, sws):
        po = cls()
        # saleschannel / mandant
        plentyid = cls.get_plenty_id(so)
        po['plentyId'] = plentyid
        # saleschannel / referrer
        refid = cls.get_plenty_referrer(so)
        po['referrerId'] = refid
        # orderid
        po.set_external_order_id(so['data']['attributes']['orderNumber'])
        # addresses
        billaddr = so.get_billing_address()
        deladdr = so.get_delivery_address()
        if deladdr != billaddr:
            pa = cls.plenty_address_from_shopware_address(deladdr)
            po.set_deliveryaddress(pa)
        pa = cls.plenty_address_from_shopware_address(billaddr)
        # email
        email = so['order-customer']['data'][0]['attributes']['email']
        pa.set_email_address(email)
        po.set_billingaddress(pa)
        # payment method
        pm = so['transactions']['data'][0]['attributes']['paymentMethodId']
        po.set_paymentmethod(PAYMENTMAPPING_SW_TO_PLENTY[pm][1])
        # orderitems
        for lineitem in so['lineItems']['data']:
            poi = cls.plenty_orderitem_from_shopware_lineitem(lineitem, refid,
                                                              sws, plenty)
            if poi:
                po.add_order_item(poi)
        # date
        timestamp = so['data']['attributes']['createdAt']
        shortenedtimestamp = timestamp[0:19] + timestamp[-6:]
        po.set_date_created_on(shortenedtimestamp)
        # shipping costs
        po.set_shipping_costs(so['data']['attributes']['shippingTotal'])
        # set status depending on payment
        paystateid = so['transactions']['data'][0]['attributes']['stateId']
        r = sws.get_state_machine_state(paystateid)
        paystate = r.json()['data']['attributes']['technicalName']
        if paystate == 'paid':
            po['statusId'] = 4
        return po


class ShopwareToPlentyOrderSync:

    def __init__(self, plentyorder, shopwareorder, plenty, sws):
        self.plentyorder = plentyorder
        self.shopwareorder = shopwareorder
        self.plenty = plenty
        self.sws = sws

    def create(self):
        po = GomazingOrder.from_shopware_order(self.shopwareorder,
                                               self.plenty, self.sws)
        po.export_to_plenty(self.plenty)
        ispre = po.contains_preorder_items()
        if ispre:
            self.shopwareorder.set_state_name('preorder')
        else:
            self.shopwareorder.set_state_name('process')

    def update_shopware_status(self):
        so = self.shopwareorder
        swstate = so.get_state_name()
        if (self.plentyorder['statusId'] == 7 and
                (swstate == 'in_progress'
                 or swstate == 'preordered'
                 or swstate == 'open')):
            if so.get_delivery_state_name() != 'shipped':
                so.set_delivery_state_name('ship')
            so.set_state_name('process')
            so.set_state_name('complete')

    def update_shopware_parcel_number(self):
        plentytracking = set(self.plentyorder.get_parcel_numbers())
        swtracking = set(self.shopwareorder.get_tracking_codes())
        if plentytracking and plentytracking != swtracking:
            self.shopwareorder.set_delivery(list(plentytracking))
            self.shopwareorder.save()

    def create_plenty_payment(self):
        swo = self.shopwareorder
        attrs = swo['transactions']['data'][0]['attributes']
        amount = attrs['amount']['totalPrice']
        recvdate = datetime.fromisoformat(attrs['updatedAt'])
        method, _ = PAYMENTMAPPING_SW_TO_PLENTY[swo.get_payment_method_id()]
        if method == 'AmazonPay':
            transid = attrs['customFields']['swag_amazon_pay_'
                                            'charge_permission_id']
            transtext = f"Complete {transid}"
            return PlentyAmazonPayPayment(amount, recvdate, transid, transtext)
        elif method == 'Bereits bezahlt':
            mollie = attrs['customFields'].get('mollie_payments', {})
            transid = 'mollie payment ' + mollie.get('payment_id', '')
            transtext = f"Complete {transid}"
            return PlentyManualPayment(amount, recvdate, transid, transtext)

    def update_plenty_payment(self):
        payment = self.create_plenty_payment()
        if payment:
            payment.export_to_plenty(self.plenty)
            payment.assign_to_order(self.plenty, self.plentyorder['id'])

    def update(self):
        if self.shopwareorder and not self.plentyorder:
            self.create()
        elif self.shopwareorder and self.plentyorder:
            self.update_shopware_parcel_number()
            self.update_shopware_status()
            if not self.plentyorder.is_paid and self.shopwareorder.is_paid():
                self.update_plenty_payment()
            if self.plentyorder.is_paid and not self.shopwareorder.is_paid():
                # TODO sync vorkasse payments
                self.shopwareorder.set_payment_state_name('paid')

    @classmethod
    def get_by_shopware_orderid(cls, sworderid, plenty, sws):
        sworder = ShopwareOrder.get_by_orderid(sworderid, sws)
        porder = PlentyOrder.get_by_externalorderid(
            plenty, sworderid,
            withparam='addresses,shippingPackages')
        return cls(porder, sworder, plenty, sws)

    @classmethod
    def get_by_plenty_orderid(cls, porderid, plenty, sws):
        porder = PlentyOrder.get_by_orderid_with_search(
            plenty, porderid,
            withparam='addresses,shippingPackages')
        sworderid = porder.get_external_order_id()
        sworder = ShopwareOrder.get_by_orderid(sworderid, sws)
        return cls(porder, sworder, plenty, sws)


@withplenty
@withshopware
def cmd_syncvariations(args, plenty=None, sws=None):
    itemids = set()
    now = datetime.now(pytz.timezone('Europe/Berlin'))
    if not args.all:
        if args.plentyitems:
            for itemid in args.plentyitems.split(','):
                itemids.add(itemid)
        else:
            try:
                laststr = open('lastshopwaresync.txt'
                               ).read().removesuffix('\n')
                last = datetime.fromisoformat(laststr)
            except FileNotFoundError:
                last = now
            variations = PlentyVariation.get_minimal_changed_since(
                plenty, last, 'variationMarkets')
            for v in variations:
                if v.get_market_active_by_id(39):
                    itemids.add(v['itemId'])
    else:
        itemids = PlentyShopwareItems.from_csv_urls(PLENTYSTOCKCSVURLS)
    total = len(itemids)
    for count, itemid in enumerate(itemids, start=1):
        info(f"Starting to process itemid {itemid} [{count}/{total}]")
        try:
            sync = PlentyToShopwareItemSyncSimple.by_plenty_itemid(
                itemid, plenty, sws)
            sync.update()
        except PlentySyncException as pse:
            warning(f"Could not sync itemid {itemid} because of {str(pse)}")
            if args.ignorewarnings:
                continue
            else:
                raise pse
        info(f"Done processing {itemid}.")
    if not args.plentyitems:
        open('lastshopwaresync.txt', 'w').write(str(now))


@withplenty
@withshopware
def cmd_syncorders(args, plenty=None, sws=None):
    orderids = args.orderids or []
    porderids = [int(oid) for oid in orderids if oid.isdecimal()]
    sorderids = [oid for oid in orderids if not oid.isdecimal()]
    if not args.orderids:
        sorderids = ShopwareOrderList.get_syncable_ordernumbers(sws)
        porderids = PlentyOrder.get_unpaid_orderids(
            plenty, referrers=CHANNELTOREFERRERMAP.values())
    for sorderid in sorderids:
        info(f"Processing {sorderid} .. ")
        sync = ShopwareToPlentyOrderSync.get_by_shopware_orderid(
            sorderid, plenty, sws)
        sync.update()
    for porderid in porderids:
        info(f"Processing {porderid} .. ")
        sync = ShopwareToPlentyOrderSync.get_by_plenty_orderid(
            porderid, plenty, sws)
        sync.update()
