mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[ADD] stock_available_sale
Take sale quotations into account in the stock quantity available to promise
Cherry-picked from 497068f5f5
Conflicts:
stock_available/res_config.py
This commit is contained in:
committed by
Lionel Sausin
parent
6cc05a8215
commit
bd3c969bfc
@@ -31,6 +31,14 @@ class StockConfig(models.TransientModel):
|
||||
"available to promise.\n"
|
||||
"This installs the module stock_available_immediately.")
|
||||
|
||||
module_stock_available_sale = fields.Boolean(
|
||||
string='Exclude goods already in sale quotations',
|
||||
help="This will subtract quantities from the sale quotations from "
|
||||
"the quantities available to promise.\n"
|
||||
"This installs the modules stock_available_sale.\n"
|
||||
"If the modules sale and sale_delivery_date are not "
|
||||
"installed, this will install them too")
|
||||
|
||||
# module_stock_available_mrp = fields.Boolean(
|
||||
# string='Include the production potential',
|
||||
# help="This will add the quantities of goods that can be "
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<field name="module_stock_available_immediately" class="oe_inline" />
|
||||
<label for="module_stock_available_immediately" />
|
||||
</div>
|
||||
<!-- <div>
|
||||
<field name="module_stock_available_sale" class="oe_inline" />
|
||||
<label for="module_stock_available_sale" />
|
||||
</div> -->
|
||||
<!-- <div>
|
||||
<field name="module_stock_available_mrp" class="oe_inline" />
|
||||
<label for="module_stock_available_mrp" />
|
||||
|
||||
21
stock_available_sale/__init__.py
Normal file
21
stock_available_sale/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import product
|
||||
44
stock_available_sale/__openerp__.py
Normal file
44
stock_available_sale/__openerp__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'Quotations in quantity available to promise',
|
||||
'version': '2.0',
|
||||
'author': u'Numérigraphe SÀRL',
|
||||
'category': 'Hidden',
|
||||
'depends': [
|
||||
'stock_available',
|
||||
'sale_order_dates',
|
||||
'sale_stock',
|
||||
],
|
||||
'description': """
|
||||
This module computes the quoted quantity of the Products, and subtracts it from
|
||||
the quantities available to promise .
|
||||
|
||||
"Quoted" is defined as the sum of the quantities of this product in Quotations,
|
||||
taking the context's shop or warehouse into account.""",
|
||||
'data': [
|
||||
'product_view.xml',
|
||||
],
|
||||
'test': [
|
||||
'test/quoted_qty.yml',
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
}
|
||||
49
stock_available_sale/i18n/fr.po
Normal file
49
stock_available_sale/i18n/fr.po
Normal file
@@ -0,0 +1,49 @@
|
||||
# Translation of OpenERP Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_sale
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 7.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-07-30 19:35+0000\n"
|
||||
"PO-Revision-Date: 2014-07-30 19:35+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: code:_description:0
|
||||
#: model:ir.model,name:stock_available_sale.model_product_product
|
||||
#, python-format
|
||||
msgid "Product"
|
||||
msgstr "Article"
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: field:product.product,quoted_qty:0
|
||||
msgid "Quoted"
|
||||
msgstr "En devis"
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: code:_description:0
|
||||
#: model:ir.model,name:stock_available_sale.model_sale_order_line
|
||||
#, python-format
|
||||
msgid "Sales Order Line"
|
||||
msgstr "Ligne de commandes de vente"
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: help:product.product,quoted_qty:0
|
||||
msgid "Total quantity of this Product that have been included in Quotations (Draft Sale Orders).\n"
|
||||
"In a context with a single Shop, this includes the Quotation processed at this Shop.\n"
|
||||
"In a context with a single Warehouse, this includes Quotation processed in any Shop using this Warehouse.\n"
|
||||
"In a context with a single Stock Location, this includes Quotation processed at any shop using any Warehouse using this Location, or any of its children, as it's Stock Location.\n"
|
||||
"Otherwise, this includes every Quotation."
|
||||
msgstr "Quantité totale de cet article se trouvant déjà dans les devis (commandes de vente brouillon).\n"
|
||||
"Quand le contexte indique une boutique, cela comprend les devis traités dans cette boutique.\n"
|
||||
"Quand le contexte indique un entrepôt, cela comprend les devis traités dans toutes les boutiques utilisant cet entrepôt.\n"
|
||||
"Quand le contexte indique un emplacement de stock, cela comprend les devis traités dans toutes les boutiques utilisant un des entrepôt utilisant cet emplacement, ou un de ses enfants, comme emplacement de stock.\n"
|
||||
"Dans les autres cas, cela comprend tous les devis."
|
||||
|
||||
45
stock_available_sale/i18n/stock_available_sale.pot
Normal file
45
stock_available_sale/i18n/stock_available_sale.pot
Normal file
@@ -0,0 +1,45 @@
|
||||
# Translation of OpenERP Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_sale
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 7.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-07-30 19:42+0000\n"
|
||||
"PO-Revision-Date: 2014-07-30 19:42+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: code:_description:0
|
||||
#: model:ir.model,name:stock_available_sale.model_product_product
|
||||
#, python-format
|
||||
msgid "Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: field:product.product,quoted_qty:0
|
||||
msgid "Quoted"
|
||||
msgstr ""
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: code:_description:0
|
||||
#: model:ir.model,name:stock_available_sale.model_sale_order_line
|
||||
#, python-format
|
||||
msgid "Sales Order Line"
|
||||
msgstr ""
|
||||
|
||||
#. module: stock_available_sale
|
||||
#: help:product.product,quoted_qty:0
|
||||
msgid "Total quantity of this Product that have been included in Quotations (Draft Sale Orders).\n"
|
||||
"In a context with a single Shop, this includes the Quotation processed at this Shop.\n"
|
||||
"In a context with a single Warehouse, this includes Quotation processed in any Shop using this Warehouse.\n"
|
||||
"In a context with a single Stock Location, this includes Quotation processed at any shop using any Warehouse using this Location, or any of its children, as it's Stock Location.\n"
|
||||
"Otherwise, this includes every Quotation."
|
||||
msgstr ""
|
||||
|
||||
232
stock_available_sale/product.py
Normal file
232
stock_available_sale/product.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp.osv import orm, fields
|
||||
import openerp.addons.decimal_precision as dp
|
||||
|
||||
# Function which uses the pool to call the method from the other modules too.
|
||||
from openerp.addons.stock_available import _product_available_fnct
|
||||
|
||||
|
||||
class ProductProduct(orm.Model):
|
||||
"""Add the computation for the stock available to promise"""
|
||||
_inherit = 'product.product'
|
||||
|
||||
def _product_available(self, cr, uid, ids, field_names=None, arg=False,
|
||||
context=None):
|
||||
"""Compute the quantities in Quotations."""
|
||||
# Compute the core quantities
|
||||
res = super(ProductProduct, self)._product_available(
|
||||
cr, uid, ids, field_names=field_names, arg=arg, context=context)
|
||||
# If we didn't get a field_names list, there's nothing to do
|
||||
if field_names is None:
|
||||
return res
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
# Prepare an alternative context without 'uom', to avoid cross-category
|
||||
# conversions when reading the available stock of components
|
||||
if 'uom' in context:
|
||||
context_wo_uom = context.copy()
|
||||
del context_wo_uom['uom']
|
||||
else:
|
||||
context_wo_uom = context
|
||||
# Compute the quantities quoted/available to promise
|
||||
if any([f in field_names
|
||||
for f in ['quoted_qty', 'immediately_usable_qty']]):
|
||||
date_str, date_args = self._get_dates(cr, uid, ids,
|
||||
context=context)
|
||||
|
||||
# Limit the search to some shops according to the context
|
||||
shop_str, shop_args = self._get_shops(cr, uid, ids,
|
||||
context=context)
|
||||
|
||||
# Query the total by Product and UoM
|
||||
cr.execute(
|
||||
"""
|
||||
SELECT sum(product_uom_qty), product_id, product_uom
|
||||
FROM sale_order_line
|
||||
INNER JOIN sale_order
|
||||
ON (sale_order_line.order_id = sale_order.id)
|
||||
WHERE product_id in %s
|
||||
AND sale_order_line.state = 'draft' """
|
||||
+ date_str + shop_str +
|
||||
"GROUP BY sale_order_line.product_id, product_uom",
|
||||
(tuple(ids),) + date_args + shop_args)
|
||||
results = cr.fetchall()
|
||||
|
||||
# Get the UoM resources we'll need for conversion
|
||||
# UoMs from the products
|
||||
uoms_o = {}
|
||||
product2uom = {}
|
||||
for product in self.browse(cr, uid, ids, context=context):
|
||||
product2uom[product.id] = product.uom_id
|
||||
uoms_o[product.uom_id.id] = product.uom_id
|
||||
# UoM from the results and the context
|
||||
uom_obj = self.pool['product.uom']
|
||||
uoms = map(lambda stock_product_uom_qty: stock_product_uom_qty[2],
|
||||
results)
|
||||
if context.get('uom', False):
|
||||
uoms.append(context['uom'])
|
||||
uoms = filter(lambda stock_product_uom_qty:
|
||||
stock_product_uom_qty not in uoms_o.keys(), uoms)
|
||||
if uoms:
|
||||
uoms = uom_obj.browse(cr, SUPERUSER_ID, list(set(uoms)),
|
||||
context=context)
|
||||
for o in uoms:
|
||||
uoms_o[o.id] = o
|
||||
|
||||
# Compute the quoted quantity
|
||||
for (amount, prod_id, prod_uom) in results:
|
||||
# Convert the amount to the product's UoM without rounding
|
||||
amount = amount / uoms_o[prod_uom].factor
|
||||
if 'quoted_qty' in field_names:
|
||||
res[prod_id]['quoted_qty'] -= amount
|
||||
if 'immediately_usable_qty' in field_names:
|
||||
res[prod_id]['immediately_usable_qty'] -= amount
|
||||
|
||||
# Round and optionally convert the results to the requested UoM
|
||||
for prod_id, stock_qty in res.iteritems():
|
||||
if context.get('uom', False):
|
||||
# Convert to the requested UoM
|
||||
res_uom = uoms_o[context['uom']]
|
||||
else:
|
||||
# The conversion is unneeded but we do need the rounding
|
||||
res_uom = product2uom[prod_id]
|
||||
if 'quoted_qty' in field_names:
|
||||
stock_qty['quoted_qty'] = uom_obj._compute_qty_obj(
|
||||
cr, SUPERUSER_ID, product2uom[prod_id],
|
||||
stock_qty['quoted_qty'],
|
||||
res_uom)
|
||||
if 'immediately_usable_qty' in field_names:
|
||||
stock_qty['immediately_usable_qty'] = \
|
||||
uom_obj._compute_qty_obj(
|
||||
cr, SUPERUSER_ID, product2uom[prod_id],
|
||||
stock_qty['immediately_usable_qty'],
|
||||
res_uom)
|
||||
return res
|
||||
|
||||
def _get_shops(self, cr, uid, ids, context=None):
|
||||
"""Find the shops matching the current context
|
||||
|
||||
See the helptext for the field quoted_qty for details"""
|
||||
shop_ids = []
|
||||
# Account for one or several locations in the context
|
||||
# Take any shop using any warehouse that has these locations as stock
|
||||
# location
|
||||
if context.get('location', False):
|
||||
# Either a single or multiple locations can be in the context
|
||||
if not isinstance(context['location'], list):
|
||||
location_ids = [context['location']]
|
||||
else:
|
||||
location_ids = context['location']
|
||||
# Add the children locations
|
||||
if context.get('compute_child', True):
|
||||
child_location_ids = self.pool['stock.location'].search(
|
||||
cr, SUPERUSER_ID,
|
||||
[('location_id', 'child_of', location_ids)])
|
||||
location_ids = child_location_ids or location_ids
|
||||
# Get the corresponding Shops
|
||||
cr.execute(
|
||||
"""
|
||||
SELECT id FROM sale_shop
|
||||
WHERE warehouse_id IN (
|
||||
SELECT id
|
||||
FROM stock_warehouse
|
||||
WHERE lot_stock_id IN %s)""",
|
||||
(tuple(location_ids),))
|
||||
res_location = cr.fetchone()
|
||||
if res_location:
|
||||
shop_ids.append(res_location)
|
||||
|
||||
# Account for a warehouse in the context
|
||||
# Take any draft order in any shop using this warehouse
|
||||
if context.get('warehouse', False):
|
||||
cr.execute("SELECT id "
|
||||
"FROM sale_shop "
|
||||
"WHERE warehouse_id = %s",
|
||||
(int(context['warehouse']),))
|
||||
res_wh = cr.fetchone()
|
||||
if res_wh:
|
||||
shop_ids.append(res_wh)
|
||||
|
||||
# If we are in a single Shop context, only count the quotations from
|
||||
# this shop
|
||||
if context.get('shop', False):
|
||||
shop_ids.append(context['shop'])
|
||||
# Build the SQL to restrict to the selected shops
|
||||
shop_str = ''
|
||||
if shop_ids:
|
||||
shop_str = 'AND sale_order.shop_id IN %s'
|
||||
|
||||
if shop_ids:
|
||||
shop_ids = (tuple(shop_ids),)
|
||||
else:
|
||||
shop_ids = ()
|
||||
return shop_str, shop_ids
|
||||
|
||||
def _get_dates(self, cr, uid, ids, context=None):
|
||||
"""Build SQL criteria to match the context's from/to dates"""
|
||||
# If we are in a context with dates, only consider the quotations to be
|
||||
# delivered at these dates.
|
||||
# If no delivery date was entered, use the order date instead
|
||||
if not context:
|
||||
return '', ()
|
||||
|
||||
from_date = context.get('from_date', False)
|
||||
to_date = context.get('to_date', False)
|
||||
date_str = ''
|
||||
date_args = []
|
||||
if from_date:
|
||||
date_str = """AND COALESCE(
|
||||
sale_order.requested_date,
|
||||
sale_order.date_order) >= %s """
|
||||
date_args.append(from_date)
|
||||
if to_date:
|
||||
date_str += """AND COALESCE(
|
||||
sale_order.requested_date,
|
||||
sale_order.date_order) <= %s """
|
||||
date_args.append(to_date)
|
||||
|
||||
if date_args:
|
||||
date_args = (tuple(date_args),)
|
||||
else:
|
||||
date_args = ()
|
||||
return date_str, date_args
|
||||
|
||||
_columns = {
|
||||
'quoted_qty': fields.function(
|
||||
_product_available_fnct, method=True, multi='qty_available',
|
||||
type='float',
|
||||
digits_compute=dp.get_precision('Product Unit of Measure'),
|
||||
string='Quoted',
|
||||
help="Total quantity of this Product that have been included in "
|
||||
"Quotations (Draft Sale Orders).\n"
|
||||
"In a context with a single Shop, this includes the "
|
||||
"Quotation processed at this Shop.\n"
|
||||
"In a context with a single Warehouse, this includes "
|
||||
"Quotation processed in any Shop using this Warehouse.\n"
|
||||
"In a context with a single Stock Location, this includes "
|
||||
"Quotation processed at any shop using any Warehouse using "
|
||||
"this Location, or any of its children, as it's Stock "
|
||||
"Location.\n"
|
||||
"Otherwise, this includes every Quotation."),
|
||||
}
|
||||
19
stock_available_sale/product_view.xml
Normal file
19
stock_available_sale/product_view.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- Add the quantity available to promise in the product form -->
|
||||
<record id="view_product_form_quoted_qty" model="ir.ui.view">
|
||||
<field name="name">product.form.quoted_qty</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="stock_available.view_stock_available_form" />
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='immediately_usable_qty']" position="after">
|
||||
<field name="quoted_qty"/>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
66
stock_available_sale/test/quoted_qty.yml
Normal file
66
stock_available_sale/test/quoted_qty.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
- Test the computation of the quoted quantity on product.product_product_10
|
||||
|
||||
- Create a UoM in the category of PCE
|
||||
- !record {model: product.uom, id: thousand}:
|
||||
name: Thousand
|
||||
factor: 0.001
|
||||
rounding: 0.00
|
||||
uom_type: bigger
|
||||
category_id: product.product_uom_categ_unit
|
||||
|
||||
- Cancel all the previous Quotations
|
||||
- !python {model: sale.order}: |
|
||||
line_ids = self.pool['sale.order.line'].search(
|
||||
cr, uid, [('product_id', '=', ref('product.product_product_10')),
|
||||
('state', '=', 'draft')])
|
||||
ids = [l.order_id.id for l in self.pool['sale.order.line'].browse(cr, uid, line_ids)]
|
||||
if ids:
|
||||
self.action_cancel(cr, uid, ids)
|
||||
- The quoted quantity should be 0
|
||||
- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
|
||||
- quoted_qty == 0.0
|
||||
|
||||
- Enter a Quotation
|
||||
- !record {model: sale.order, id: order1}:
|
||||
order_line:
|
||||
- name: Quotation 1
|
||||
product_uom: product.product_uom_unit
|
||||
product_uom_qty: 107.0
|
||||
state: draft
|
||||
product_id: product.product_product_10
|
||||
partner_id: base.res_partner_2
|
||||
partner_invoice_id: base.res_partner_address_8
|
||||
partner_shipping_id: base.res_partner_address_8
|
||||
pricelist_id: product.list0
|
||||
- The quoted qty should match the single quotation
|
||||
- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
|
||||
- quoted_qty == -107.0
|
||||
|
||||
- Enter another Quotation
|
||||
- !record {model: sale.order, id: order2}:
|
||||
order_line:
|
||||
- name: Quotation 1
|
||||
product_uom: thousand
|
||||
product_uom_qty: 0.613
|
||||
state: draft
|
||||
product_id: product.product_product_10
|
||||
partner_id: base.res_partner_2
|
||||
partner_invoice_id: base.res_partner_address_9
|
||||
partner_shipping_id: base.res_partner_address_9
|
||||
pricelist_id: product.list0
|
||||
- The quoted qty should match the total of the quotations
|
||||
- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
|
||||
- quoted_qty == -720.0
|
||||
- Use the context to report in another UoM
|
||||
- !assert {model: product.product, id: product.product_product_10, string: "Check in other UoM", context: "{'uom': ref('thousand')}"}:
|
||||
- quoted_qty == -0.72
|
||||
- Use the context to report in the default UoM
|
||||
- !assert {model: product.product, id: product.product_product_10, string: "Check in False UoM", context: "{'uom': False}"}:
|
||||
- quoted_qty == -720.0
|
||||
|
||||
- Confirm one of the Quotations
|
||||
- !workflow {model: sale.order, action: order_confirm, ref: order1}
|
||||
- The quoted qty should match the remaining quotation
|
||||
- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
|
||||
- quoted_qty == -613.0
|
||||
|
||||
Reference in New Issue
Block a user