예제 #1
0
def stock_info(item):
    auth = current.auth
    T = current.T
    session = current.session

    available = True
    stock = 0

    if auth.has_membership('Employee') and item.has_inventory:
        stock = item_stock_qty(item, session.store)
        stock = fix_item_quantity(item, stock)
        if stock <= 0:
            stock = SPAN(T('Out of stock'), _class="text-danger")
            available = False
        else:
            stock = str(stock) + " %s %s " % (item.id_measure_unit.symbol,
                                              T('Available'))
    else:
        stock = item_stock_qty(item)
        if stock <= 0:
            stock = SPAN(T('Out of stock'), _class="text-danger")
            available = False
        else:
            stock = SPAN(T('Available'), _class="text-success")

    return stock
예제 #2
0
def stock_info(item):
    auth = current.auth
    T = current.T
    session = current.session

    available = True
    stock = 0

    if auth.has_membership('Employee') and item.has_inventory:
        stock = item_stock_qty(item, session.store)
        stock = fix_item_quantity(item, stock)
        if stock <= 0:
            stock = SPAN(T('Out of stock'), _class="text-danger")
            available = False
        else:
            stock = str(stock) + " %s %s " % (item.id_measure_unit.symbol, T('Available'))
    else:
        stock = item_stock_qty(item)
        if stock <= 0:
            stock = SPAN(T('Out of stock'), _class="text-danger")
            available = False
        else:
            stock = SPAN(T('Available'), _class="text-success")

    return stock
예제 #3
0
def modify_bag_item():
    """
        modifies the bag_item quantity.
        args: [ bag_item ]

    """

    bag_item = db.bag_item(request.args(0))
    if not bag_item:
        raise HTTP(404)
    bag_utils.is_modifiable_bag(bag_item.id_bag)

    old_qty = bag_item.quantity
    bag_item.quantity = request.vars.quantity if request.vars.quantity else bag_item.quantity
    if not bag_item.id_item.allow_fractions:
        bag_item.quantity = remove_fractions(bag_item.quantity)
    bag_item.quantity = DQ(bag_item.quantity)

    if not allow_out_of_stock:
        qty = item_utils.item_stock_qty(
            db.item(bag_item.id_item), session.store, id_bag=session.current_bag
        )
        diff = (old_qty - bag_item.quantity) if (old_qty - bag_item.quantity) > 0 else 0
        if qty + diff < bag_item.quantity - old_qty:
            bag_item.quantity = max(old_qty, qty + old_qty)
    bag_item.quantity = max(0, bag_item.quantity)

    bag_item.update_record()
    bag_data = bag_utils.refresh_bag_data(bag_item.id_bag.id)
    return dict(status='ok', bag_item=bag_item, **bag_data)
예제 #4
0
def modify_bag_item():
    """
        modifies the bag_item quantity.
        args: [ bag_item ]

    """

    bag_item = db.bag_item(request.args(0))
    bag_utils.is_modifiable_bag(bag_item.id_bag)
    if not bag_item:
        raise HTTP(404)

    old_qty = bag_item.quantity
    bag_item.quantity = request.vars.quantity if request.vars.quantity else bag_item.quantity
    if not bag_item.id_item.allow_fractions:
        bag_item.quantity = remove_fractions(bag_item.quantity)
    bag_item.quantity = DQ(bag_item.quantity)

    if not allow_out_of_stock:
        qty = item_utils.item_stock_qty(
            db.item(bag_item.id_item), session.store, id_bag=session.current_bag
        )
        diff = (old_qty - bag_item.quantity) if (old_qty - bag_item.quantity) > 0 else 0
        if qty + diff < bag_item.quantity - old_qty:
            bag_item.quantity = max(old_qty, qty + old_qty)
    bag_item.quantity = max(0, bag_item.quantity)

    bag_item.update_record()
    bag_data = bag_utils.refresh_bag_data(bag_item.id_bag.id)
    return dict(status='ok', bag_item=bag_item, **bag_data)
예제 #5
0
def out_of_stock_items_exists(bag_items, allow_out_of_stock):
    if allow_out_of_stock:
        return False

    for bag_item in bag_items:
        qty = item_utils.item_stock_qty(bag_item.id_item,
                                        bag_item.id_bag.id_store.id)
        if bag_item.quantity > qty:
            return True

    return False
예제 #6
0
def out_of_stock_items_exists(bag_items, allow_out_of_stock):
    if allow_out_of_stock:
        return False

    for bag_item in bag_items:
        qty = item_utils.item_stock_qty(
            bag_item.id_item, bag_item.id_bag.id_store.id
        )
        if bag_item.quantity > qty:
            return True

    return False
예제 #7
0
def add_bag_item(bag, item, quantity=None, sale_price=None):
    """ """

    db = current.db

    sale_price = sale_price if sale_price else item.base_price

    bag_item = db((db.bag_item.id_item == item.id)
                  & (db.bag_item.id_bag == bag.id)).select().first()

    # when quantity is not specified, avoid stock checking (the API user knows what he is doing)
    if not quantity:
        stock_qty = item_utils.item_stock_qty(item,
                                              bag.id_store,
                                              id_bag=bag.id)
        if item.has_inventory:
            stock_qty = DQ(stock_qty)

        base_qty = base_qty = 1 if stock_qty >= 1 or ALLOW_OUT_OF_STOCK else stock_qty % 1  # modulo to consider fractionary items
        # if there is no stock notify the user
        if base_qty <= 0:
            raise CP_OutOfStockError()
        quantity = base_qty

    # create item taxes string, the string contains the tax name and its percentage, see db.py > bag_item table for more info
    if not bag_item:
        item_taxes_str = ''
        for tax in item.taxes:
            item_taxes_str += '%s:%s' % (tax.name, tax.percentage)
            if tax != item.taxes[-1]:
                item_taxes_str += ','
        discounts = item_utils.item_discounts(item)
        sale_price = item_utils.discount_data(discounts, sale_price)[0]
        discount = item.base_price - sale_price
        id_bag_item = db.bag_item.insert(id_bag=bag.id,
                                         id_item=item.id,
                                         quantity=quantity,
                                         sale_price=sale_price,
                                         discount=discount,
                                         product_name=item.name,
                                         item_taxes=item_taxes_str,
                                         sale_taxes=item_utils.item_taxes(
                                             item, sale_price))
        bag_item = db.bag_item(id_bag_item)
    else:
        bag_item.quantity += base_qty
        bag_item.update_record()

    return bag_item
예제 #8
0
def add_bag_item(bag, item, quantity=None, sale_price=None):
    """ """

    db = current.db

    sale_price = sale_price if sale_price else item.base_price

    bag_item = db(
          (db.bag_item.id_item == item.id)
        & (db.bag_item.id_bag == bag.id)
    ).select().first()

    # when quantity is not specified, avoid stock checking (the API user knows what he is doing)
    if not quantity:
        stock_qty = item_utils.item_stock_qty(item, bag.id_store, id_bag=bag.id)
        if item.has_inventory:
            stock_qty = DQ(stock_qty)

        base_qty = base_qty = 1 if stock_qty >= 1 or ALLOW_OUT_OF_STOCK else stock_qty % 1 # modulo to consider fractionary items
        # if there is no stock notify the user
        if base_qty <= 0:
            raise CP_OutOfStockError()
        quantity = base_qty

    # create item taxes string, the string contains the tax name and its percentage, see db.py > bag_item table for more info
    if not bag_item:
        item_taxes_str = ''
        for tax in item.taxes:
            item_taxes_str += '%s:%s' % (tax.name, tax.percentage)
            if tax != item.taxes[-1]:
                item_taxes_str += ','
        discounts = item_utils.item_discounts(item)
        sale_price = item_utils.discount_data(discounts, sale_price)[0]
        discount = item.base_price - sale_price
        id_bag_item = db.bag_item.insert(
            id_bag=bag.id, id_item=item.id, quantity=quantity,
            sale_price=sale_price, discount=discount,
            product_name=item.name, item_taxes=item_taxes_str,
            sale_taxes=item_utils.item_taxes(item, sale_price),
            reward_points=item.reward_points or 0
        )
        bag_item = db.bag_item(id_bag_item)
    else:
        bag_item.quantity += base_qty
        bag_item.update_record()

    return bag_item
예제 #9
0
def set_bag_item(bag_item, discounts=None):
    """ modifies bag item data, in order to display it properly, this method does not modify the database """

    if discounts is None:
        discounts = []

    session = current.session

    item = bag_item.id_item

    bag_item.product_name = item.name + " " + item_utils.concat_traits(item)
    if discounts is None:
        discounts = []

    # stores the price without discounts
    real_price = bag_item.sale_price + (bag_item.discount or 0)
    # discount percentage
    discount_p = 0
    try:
        discount_p = DQ(1.0) - bag_item.sale_price / real_price
    except:
        pass
    item.base_price -= item.base_price * discount_p

    bag_item.total_sale_price = str(
        DQ(bag_item.sale_price + bag_item.sale_taxes, True))
    bag_item.base_price = money_format(DQ(item.base_price,
                                          True)) if item.base_price else 0
    bag_item.price2 = money_format(
        DQ(item.price2 - item.price2 * discount_p, True)) if item.price2 else 0
    bag_item.price3 = money_format(
        DQ(item.price3 - item.price3 * discount_p, True)) if item.price3 else 0
    bag_item.sale_price = money_format(DQ(bag_item.sale_price or 0, True))

    # add taxes without discounts
    real_price += bag_item_taxes(bag_item, real_price)
    bag_item.price_no_discount = real_price

    bag_item.measure_unit = item.id_measure_unit.symbol

    bag_item.barcode = item_utils.item_barcode(item)
    bag_item.stock = item_utils.item_stock_qty(item, session.store)
    bag_item.has_inventory = item.has_inventory
    bag_item.discount_percentage = int(discount_p * D(100.0))
    bag_item.real_price = bag_item.sale_price

    return bag_item
예제 #10
0
def set_bag_item(bag_item, discounts=None):
    """ modifies bag item data, in order to display it properly, this method does not modify the database """

    if discounts is None:
        discounts = []

    session = current.session

    item = bag_item.id_item

    bag_item.product_name = item.name + " " + item_utils.concat_traits(item)
    if discounts is None:
        discounts = []

    # stores the price without discounts
    real_price = bag_item.sale_price + (bag_item.discount or 0)
    # discount percentage
    discount_p = 0
    try:
        discount_p = DQ(1.0) - bag_item.sale_price / real_price
    except:
        pass
    item.base_price -= item.base_price * discount_p

    bag_item.total_sale_price = str(DQ(bag_item.sale_price + bag_item.sale_taxes, True))
    bag_item.base_price = money_format(DQ(item.base_price, True)) if item.base_price else 0
    bag_item.price2 = money_format(DQ(item.price2 - item.price2 * discount_p, True)) if item.price2 else 0
    bag_item.price3 = money_format(DQ(item.price3 - item.price3 * discount_p, True)) if item.price3 else 0
    bag_item.sale_price = money_format(DQ(bag_item.sale_price or 0, True))


    # add taxes without discounts
    real_price += bag_item_taxes(bag_item, real_price)
    bag_item.price_no_discount = real_price

    bag_item.measure_unit = item.id_measure_unit.symbol

    bag_item.barcode = item_utils.item_barcode(item)
    bag_item.stock = item_utils.item_stock_qty(item, session.store)
    bag_item.has_inventory = item.has_inventory
    bag_item.discount_percentage = int(discount_p * D(100.0))
    bag_item.real_price = bag_item.sale_price

    return bag_item
예제 #11
0
def check_bag_items_integrity(bag_items, allow_out_of_stock=False):
    """ verify item stocks and remove unnecessary items """
    session = current.session
    db = current.db
    auth = current.auth
    T = current.T

    out_of_stock_items = []
    for bag_item in bag_items:
        # delete bag item when the item has 0 quantity
        if bag_item.quantity <= 0:
            db(db.bag_item.id == bag_item.id).delete()

        qty = item_utils.item_stock_qty(bag_item.id_item, session.store)
        if bag_item.quantity > qty and not allow_out_of_stock:
            out_of_stock_items.append(bag_item)
    if out_of_stock_items and auth.has_membership('Employee'):
        session.flash = T('Some items are out of stock or are inconsistent')
        auto_bag_selection()
        redirection()
예제 #12
0
def check_bag_items_integrity(bag_items, allow_out_of_stock=False):
    """ verify item stocks and remove unnecessary items """
    session = current.session
    db = current.db
    auth = current.auth
    T = current.T

    out_of_stock_items = []
    for bag_item in bag_items:
        # delete bag item when the item has 0 quantity
        if bag_item.quantity <= 0:
            db(db.bag_item.id == bag_item.id).delete()

        qty = item_utils.item_stock_qty(bag_item.id_item, session.store)
        if bag_item.quantity > qty and not allow_out_of_stock:
            out_of_stock_items.append(bag_item)
    if out_of_stock_items and auth.has_membership('Employee'):
        session.flash = T('Some items are out of stock or are inconsistent')
        auto_bag_selection()
        redirection()
예제 #13
0
def item_card(item):
    """ """
    session = current.session
    auth = current.auth
    db = current.db
    T = current.T

    available = "Not available"
    available_class = "label label-danger"

    stock_qty = item_stock_qty(item, session.store)
    if stock_qty > 0:
        available_class = "label label-success"
        available = "Available"

    item_options = DIV()

    bg_style = ""
    images = db((db.item_image.id_item == db.item.id)
                & (db.item.id == item.id)
                & (db.item.is_active == True)).select(db.item_image.ALL)
    if images:
        bg_style = "background-image: url(%s);" % URL(
            'static', 'uploads/' + images.first().sm)
    else:
        bg_style = "background-image: url(%s);" % URL('static',
                                                      'images/no_image.svg')

    brand_link = H4(
        A(item.id_brand.name,
          _href=URL('item', 'browse', vars=dict(
              brand=item.id_brand.id)))) if item.id_brand else H4(
                  T('No brand'))

    item_price = (item.base_price or 0) + item_taxes(item, item.base_price)
    fix_item_price(item, item.base_price)
    item_price = item.discounted_price

    item_price_html = DIV()
    if item.discount_percentage > 0:
        item_price_html.append(
            DIV(
                T('before'),
                SPAN('$ ',
                     SPAN(item.new_price, _class="old-price"),
                     _class="right")))
        item_price_html.append(
            DIV(
                T('discount'),
                SPAN(SPAN(item.discount_percentage),
                     '%',
                     _class="right text-danger")))
    item_price_html.append(
        DIV(SPAN(T(available), _class=available_class + ' item-available'),
            H4('$ ', DQ(item_price, True), _class="item-price"),
            _class="item-card-bottom"))

    # concatenate all the item traits, this string will be appended to the item name
    traits_str = ''
    traits_ids = ''
    item_url = URL('item', 'get_item', args=item.id)
    item_name = item.name
    if item.traits:
        for trait in item.traits:
            traits_ids += str(trait.id)
            traits_str += trait.trait_option + ' '
            if trait != item.traits[-1]:
                traits_ids += ','
        item_url = URL('item',
                       'get_item',
                       vars=dict(name=item.name, traits=traits_ids))
        item_name = item.name + ' ' + traits_str
    elif item.description:
        item_name += ' - ' + item.description[:10]
        if len(item.description) > 10:
            item_name += '...'

    main_content = DIV(H4(A(item_name, _href=item_url)),
                       brand_link,
                       _class="item_data")

    # item options
    item_options = DIV(BUTTON(ICON('shopping_basket'),
                              _type="button",
                              _class="btn btn-default",
                              _onclick="add_bag_item(%s)" % item.id),
                       _class="btn-group item-options")
    if auth.has_membership('Employee'):
        main_content.append(
            P('# ', SPAN(item_barcode(item)), _class="item-barcode"))

        expand_btn = BUTTON(ICON('more_vert'),
                            _type="button",
                            _class="btn btn-default dropdown-toggle",
                            data={'toggle': 'dropdown'})
        item_options.append(expand_btn)
        options_ul = UL(_class="dropdown-menu")
        if auth.has_membership('Items info') or auth.has_membership(
                'Items management') or auth.has_membership('Items prices'):
            options_ul.append(
                LI(A(T('Update'), _href=URL('item', 'update', args=item.id))))
            options_ul.append(
                LI(
                    A(T('Print labels'),
                      _href=URL('item', 'labels', args=item.id))))
            options_ul.append(
                LI(
                    A(T('Add images'),
                      _href=URL('item_image', 'create', args=item.id))))
        if auth.has_membership('Analytics'):
            options_ul.append(
                LI(
                    A(T('Analysis'),
                      _href=URL('analytics', 'item_analysis', args=item.id))))
        item_options.append(options_ul)

    return DIV(A('', _class="panel-heading", _style=bg_style, _href=item_url),
               DIV(main_content,
                   item_options,
                   item_price_html,
                   _class="panel-body"),
               _class="panel panel-default item-card")
예제 #14
0
def item_card(item):
    """ """
    session = current.session
    auth = current.auth
    db = current.db
    T = current.T

    available = "Not available"
    available_class = "label label-danger"

    stock_qty = item_stock_qty(item, session.store)
    if stock_qty > 0:
        available_class = "label label-success"
        available = "Available"

    item_options = DIV()

    bg_style = ""
    images = db(
        (db.item_image.id_item == db.item.id)
      & (db.item.id == item.id)
      & (db.item.is_active == True)
    ).select(db.item_image.ALL)
    if images:
        bg_style = "background-image: url(%s);" % URL('static','uploads/'+ images.first().sm)
    else:
        bg_style = "background-image: url(%s);" % URL('static', 'images/no_image.svg')

    brand_link = H4(
        A(item.id_brand.name,
          _href=URL('item', 'browse', vars=dict(brand=item.id_brand.id))
        )
    ) if item.id_brand else H4(T('No brand'))

    item_price = (item.base_price or 0) + item_taxes(item, item.base_price)
    fix_item_price(item, item.base_price)
    item_price = item.discounted_price

    item_price_html = DIV()
    if item.discount_percentage > 0:
        item_price_html.append(DIV(T('before'), SPAN('$ ', SPAN(item.new_price, _class="old-price"), _class="right")))
        item_price_html.append(
            DIV(T('discount'), SPAN(SPAN(item.discount_percentage), '%', _class="right text-danger"))
        )
    item_price_html.append(
        DIV(
            SPAN(T(available), _class=available_class + ' item-available'),
            H4('$ ', DQ(item_price, True), _class="item-price"),
            _class="item-card-bottom"
        )
    )


    # concatenate all the item traits, this string will be appended to the item name
    traits_str = ''
    traits_ids = ''
    item_url = URL('item', 'get_item', args=item.id)
    item_name = item.name
    if item.traits:
        for trait in item.traits:
            traits_ids += str(trait.id)
            traits_str += trait.trait_option + ' '
            if trait != item.traits[-1]:
                traits_ids += ','
        item_url = URL('item', 'get_item', vars=dict(name=item.name, traits=traits_ids))
        item_name = item.name + ' ' + traits_str
    elif item.description:
        item_name += ' - ' + item.description[:10]
        if len(item.description) > 10:
            item_name += '...'

    main_content = DIV(
        H4(A(item_name, _href=item_url)),
        brand_link,
        _class="item_data"
    )

    # item options
    item_options = DIV(
        BUTTON(ICON('shopping_basket'), _type="button", _class="btn btn-default", _onclick="add_bag_item(%s)" % item.id)
        , _class="btn-group item-options"
    )
    if auth.has_membership('Employee'):
        main_content.append(
            P('# ', SPAN(item_barcode(item)), _class="item-barcode")
        )

        expand_btn = BUTTON(ICON('more_vert'), _type="button", _class="btn btn-default dropdown-toggle", data={'toggle':'dropdown'})
        item_options.append(expand_btn)
        options_ul = UL(_class="dropdown-menu")
        if auth.has_membership('Items info') or auth.has_membership('Items management') or auth.has_membership('Items prices'):
            options_ul.append(
                LI(A(T('Update'), _href=URL('item', 'update', args=item.id)))
            )
            options_ul.append(
                LI(A(T('Print labels'), _href=URL('item', 'labels', args=item.id)))
            )
            options_ul.append(
                LI(A(T('Add images'), _href=URL('item_image', 'create', args=item.id)))
            )
        if auth.has_membership('Analytics'):
            options_ul.append(
                LI(A(T('Analysis'), _href=URL('analytics', 'item_analysis', args=item.id)))
            )
        item_options.append(options_ul)

    return DIV(
        A('', _class="panel-heading", _style=bg_style, _href=item_url),
        DIV(
            main_content,
            item_options,
            item_price_html,
            _class="panel-body"
        ),
        _class="panel panel-default item-card"
    )
예제 #15
0
def complete(sale):
    """ """

    db = current.db
    T = current.T

    payments = db(db.payment.id_sale == sale.id).select()
    verify_payments(payments, sale)

    affinity_list = []

    # verify items stock
    bag_items = db(db.bag_item.id_bag == sale.id_bag.id).iterselect()
    requires_serials = False  #TODO implement serial numbers
    for bag_item in bag_items:
        # since created bags does not remove stock, there could be more bag_items than stock items, so we need to check if theres enough stock to satisfy this sale, and if there is not, then we need to notify the seller or user
        stock_qty = item_utils.item_stock_qty(bag_item.id_item,
                                              sale.id_store.id,
                                              bag_item.id_bag.id)
        # this has to be done since using item_stock_qty with a bag specified will
        # consider bagged items as missing, thus we have to add them back,
        # item_stock_qty must be called with a bag because that way it will
        # count bundled items with the specified item
        stock_qty += bag_item.quantity
        # Cannot deliver a sale with out of stock items
        if stock_qty < bag_item.quantity:
            raise CP_OutOfStockError(
                T("You can't create a counter sale with out of stock items"))
        requires_serials |= bag_item.id_item.has_serial_number or False

        affinity_list.append(bag_item.id_item.id)

    # kinda crappy
    first = affinity_list[0]
    for i, id in enumerate(affinity_list):
        if i == len(affinity_list) - 1:
            continue
        for j, other_id in enumerate(affinity_list):
            if j <= i:
                continue
            first, second = (id, other_id) if id < other_id else (other_id, id)

            affinity = db((db.item_affinity.id_item1 == first) & (
                db.item_affinity.id_item2 == second)).select().first()
            if not affinity:
                db.item_affinity.insert(id_item1=first,
                                        id_item2=second,
                                        affinity=1)
            else:
                affinity.affinity += 1
                affinity.update_record()

    # for every payment with a payment option with credit days, set payment to not settled
    for payment in payments:
        if payment.id_payment_opt.credit_days > 0:
            payment.epd = date(request.now.year, request.now.month,
                               request.now.day) + timedelta(
                                   days=payment.id_payment_opt.credit_days)
            payment.is_settled = False
            payment.update_record()

    store = db(db.store.id == sale.id_store.id).select(for_update=True).first()
    sale.consecutive = store.consecutive
    sale.is_done = True
    create_sale_event(sale, SALE_PAID)
    sale.update_record()
    store.consecutive += 1
    store.update_record()

    # if defered sale, remove sale order
    db(db.sale_order.id_sale == sale.id).delete()

    # add reward points to the client's wallet, we assume that the user has a wallet
    if sale.id_client and sale.id_client.id_wallet:
        wallet = db(db.wallet.id == sale.id_client.id_wallet.id).select(
            for_update=True).first()
        wallet.balance += sale.reward_points
        wallet.update_record()
예제 #16
0
def complete(sale):
    """ """

    db = current.db
    T = current.T

    payments = db(db.payment.id_sale == sale.id).select()
    verify_payments(payments, sale)


    affinity_list = []

    # verify items stock
    bag_items = db(db.bag_item.id_bag == sale.id_bag.id).iterselect()
    requires_serials = False  #TODO implement serial numbers
    for bag_item in bag_items:
        # since created bags does not remove stock, there could be more bag_items than stock items, so we need to check if theres enough stock to satisfy this sale, and if there is not, then we need to notify the seller or user
        stock_qty = item_utils.item_stock_qty(
            bag_item.id_item, sale.id_store.id, bag_item.id_bag.id
        )
        # this has to be done since using item_stock_qty with a bag specified will
        # consider bagged items as missing, thus we have to add them back,
        # item_stock_qty must be called with a bag because that way it will
        # count bundled items with the specified item
        stock_qty += bag_item.quantity
        # Cannot deliver a sale with out of stock items
        if stock_qty < bag_item.quantity:
            raise CP_OutOfStockError(
                T("You can't create a counter sale with out of stock items")
            )
        requires_serials |= bag_item.id_item.has_serial_number or False

        affinity_list.append(bag_item.id_item.id)


    # kinda crappy
    first = affinity_list[0]
    for i, id in enumerate(affinity_list):
        if i == len(affinity_list) - 1:
            continue
        for j, other_id in enumerate(affinity_list):
            if j <= i:
                continue
            first, second = (id, other_id) if id < other_id else (other_id, id)

            affinity = db(
                (db.item_affinity.id_item1 == first) &
                (db.item_affinity.id_item2 == second)
            ).select().first()
            if not affinity:
                db.item_affinity.insert(
                    id_item1=first, id_item2=second, affinity=1
                )
            else:
                affinity.affinity += 1
                affinity.update_record()


    # for every payment with a payment option with credit days, set payment to not settled
    for payment in payments:
        if payment.id_payment_opt.credit_days > 0:
            payment.epd = date(request.now.year, request.now.month, request.now.day) + timedelta(days=payment.id_payment_opt.credit_days)
            payment.is_settled = False
            payment.update_record()

    store = db(db.store.id == sale.id_store.id).select(for_update=True).first()
    sale.consecutive = store.consecutive
    sale.is_done = True
    create_sale_event(sale, SALE_PAID)
    sale.update_record()
    store.consecutive += 1
    store.update_record()

    # if defered sale, remove sale order
    db(db.sale_order.id_sale == sale.id).delete()

    # add reward points to the client's wallet, we assume that the user has a wallet
    if sale.id_client and sale.id_client.id_wallet:
        wallet_utils.transaction(
            sale.reward_points, wallet_utils.CONCEPT_SALE_REWARD, ref=sale.id,
            wallet_id=sale.id_client.id_wallet.id
        )