[IMP] Optimize orderpoint stock get + traversal of procurement subtraction + change edi format test date_planned to datetime
[IMP] Make sure dropship route can be chosen (takes prio)
This commit is contained in:
+22
-36
@@ -1210,6 +1210,28 @@ class procurement_order(osv.osv):
|
||||
return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id]
|
||||
return super(procurement_order, self)._run(cr, uid, procurement, context=context)
|
||||
|
||||
#TODO: Autocommit needed?
|
||||
def run(self, cr, uid, ids, autocommit=False, context=None):
|
||||
procs = self.browse(cr, uid, ids, context=context)
|
||||
to_assign = [x for x in procs if x.state not in ('running', 'done')]
|
||||
self._assign_multi(cr, uid, to_assign, context=context)
|
||||
buy_ids = [x.id for x in procs if x.rule_id and x.rule_id.action == 'buy']
|
||||
if buy_ids:
|
||||
result_dict = self.make_po(cr, uid, buy_ids, context=context)
|
||||
runnings = []
|
||||
exceptions = []
|
||||
for proc in result_dict.keys():
|
||||
if result_dict[proc]:
|
||||
runnings += [proc]
|
||||
else:
|
||||
exceptions += [proc]
|
||||
if runnings:
|
||||
self.write(cr, uid, runnings, {'state': 'running'}, context=context)
|
||||
if exceptions:
|
||||
self.write(cr, uid, exceptions, {'state': 'exception'}, context=context)
|
||||
set_others = set(ids) - set(buy_ids)
|
||||
return super(procurement_order, self).run(cr, uid, list(set_others), context=context)
|
||||
|
||||
def _check(self, cr, uid, procurement, context=None):
|
||||
if procurement.purchase_line_id and procurement.purchase_line_id.order_id.shipped: # TOCHECK: does it work for several deliveries?
|
||||
return True
|
||||
@@ -1290,42 +1312,6 @@ class procurement_order(osv.osv):
|
||||
''' returns the main supplier of the procurement's product given as argument'''
|
||||
return procurement.product_id.seller_id
|
||||
|
||||
def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
pricelist_obj = self.pool.get('product.pricelist')
|
||||
prod_obj = self.pool.get('product.product')
|
||||
acc_pos_obj = self.pool.get('account.fiscal.position')
|
||||
|
||||
seller_qty = procurement.product_id.seller_qty
|
||||
pricelist_id = partner.property_product_pricelist_purchase.id
|
||||
uom_id = procurement.product_id.uom_po_id.id
|
||||
qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
|
||||
if seller_qty:
|
||||
qty = max(qty, seller_qty)
|
||||
price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id]
|
||||
|
||||
#Passing partner_id to context for purchase order line integrity of Line name
|
||||
new_context = context.copy()
|
||||
new_context.update({'lang': partner.lang, 'partner_id': partner.id})
|
||||
product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
|
||||
taxes_ids = procurement.product_id.supplier_taxes_id
|
||||
taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
|
||||
name = product.display_name
|
||||
if product.description_purchase:
|
||||
name += '\n' + product.description_purchase
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'product_qty': qty,
|
||||
'product_id': procurement.product_id.id,
|
||||
'product_uom': uom_id,
|
||||
'price_unit': price or 0.0,
|
||||
'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'taxes_id': [(6, 0, taxes)],
|
||||
}
|
||||
|
||||
def _get_po_line_values_from_procs(self, cr, uid, procurements, partner, schedule_date, context=None):
|
||||
res = {}
|
||||
if context is None:
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
product_uom: 1
|
||||
price_unit: 60.0
|
||||
name: 'Headset USB'
|
||||
date_planned: '2011-08-31'
|
||||
date_planned: '2011-08-31 12:00:00'
|
||||
order_line:
|
||||
- product_id: product.product_product_29
|
||||
product_qty: 12.0
|
||||
product_uom: 1
|
||||
price_unit: 89.0
|
||||
name: 'Pen drive, 8GB'
|
||||
date_planned: '2011-08-31'
|
||||
date_planned: '2011-08-31 12:00:00'
|
||||
|
||||
-
|
||||
I confirm the purchase order
|
||||
@@ -90,7 +90,7 @@
|
||||
"name": "PC Assemble SC234",
|
||||
"product_uom": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_uom_unit", "Unit"],
|
||||
"product_qty": 1.0,
|
||||
"date_planned": "2011-09-30",
|
||||
"date_planned": "2011-09-30 12:00:00",
|
||||
"sequence": 10,
|
||||
"price_unit": 150.0,
|
||||
"product_id": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_product_3", "[PCSC234] PC Assemble SC234"],
|
||||
@@ -105,7 +105,7 @@
|
||||
"product_uom": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_uom_unit", "Unit"],
|
||||
"product_qty": 10.0,
|
||||
"sequence": 11,
|
||||
"date_planned": "2011-09-15",
|
||||
"date_planned": "2011-09-15 12:00:00",
|
||||
"price_unit": 20.0,
|
||||
"product_id": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_product_5", "[PC-DEM] PC on Demand"],
|
||||
}],
|
||||
@@ -192,7 +192,7 @@
|
||||
"name": "Basic PC",
|
||||
"product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"],
|
||||
"product_qty": 1.0,
|
||||
"date_planned": "2011-09-30",
|
||||
"date_planned": "2011-09-30 12:00:00",
|
||||
"sequence": 10,
|
||||
"price_unit": 150.0,
|
||||
"product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc1", "[PC1] Basic PC"],
|
||||
@@ -207,7 +207,7 @@
|
||||
"product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"],
|
||||
"product_qty": 10.0,
|
||||
"sequence": 11,
|
||||
"date_planned": "2011-09-15",
|
||||
"date_planned": "2011-09-15 12:00:00",
|
||||
"price_unit": 20.0,
|
||||
"product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc3", "[PC3] Medium PC"],
|
||||
}],
|
||||
|
||||
+193
-40
@@ -95,7 +95,7 @@ class procurement_order(osv.osv):
|
||||
move_cancel += [m.id for m in procurement.move_ids]
|
||||
if move_cancel:
|
||||
self.pool.get('stock.move').action_cancel(cr, uid, move_cancel, context=context)
|
||||
return super(procurement_order, self).propagate_cancels(cr, uid, ids, context=context)
|
||||
return True
|
||||
|
||||
def cancel(self, cr, uid, ids, context=None):
|
||||
if context is None:
|
||||
@@ -121,6 +121,132 @@ class procurement_order(osv.osv):
|
||||
return {'value': {'location_id': warehouse.lot_stock_id.id}}
|
||||
return {}
|
||||
|
||||
#Doing assignation, ... in multi
|
||||
def _assign_multi(self, cr, uid, procurements, context=None):
|
||||
res = {}
|
||||
todo_procs = []
|
||||
context = context or {}
|
||||
for procurement in procurements:
|
||||
if procurement.rule_id:
|
||||
res[procurement.id] = True
|
||||
elif procurement.product_id.type != 'service':
|
||||
todo_procs += [procurement]
|
||||
|
||||
res_dict = self._find_suitable_rule_multi(cr, uid, todo_procs, context=context)
|
||||
rule_dict = {}
|
||||
for proc in res_dict.keys():
|
||||
if res_dict[proc]:
|
||||
if rule_dict.get(res_dict[proc]):
|
||||
rule_dict[res_dict[proc]] += [proc]
|
||||
else:
|
||||
rule_dict[res_dict[proc]] = [proc]
|
||||
|
||||
for rule in rule_dict.keys():
|
||||
self.write(cr, uid, rule_dict[rule], {'rule_id': rule}, context=context)
|
||||
|
||||
|
||||
def _get_route_group_dict(self, cr, uid, procurements, context=None):
|
||||
"""
|
||||
Returns a dictionary with key the routes and values the products associated
|
||||
"""
|
||||
ids = [x.id for x in procurements]
|
||||
cr.execute("""
|
||||
SELECT proc_id, route_id FROM
|
||||
((SELECT p.id AS proc_id, route_id
|
||||
FROM stock_route_product AS link, procurement_order AS p, product_template AS pt, product_product pp
|
||||
WHERE pp.product_tmpl_id = pt.id AND link.product_id = pt.id AND pp.id = p.product_id
|
||||
AND p.id in %s)
|
||||
UNION (SELECT p.id AS proc_id, link.route_id AS route_id
|
||||
FROM stock_location_route_categ AS link, product_product AS pp, procurement_order AS p,
|
||||
product_template AS pt, product_category AS pc, product_category AS pc_product
|
||||
WHERE p.product_id = pp.id AND pp.product_tmpl_id = pt.id AND pc_product.id = pt.categ_id AND
|
||||
pc.parent_left <= pc_product.parent_left AND pc.parent_right >= pc_product.parent_left
|
||||
AND link.categ_id = pc.id AND pp.id IN %s)) p ORDER BY proc_id, route_id
|
||||
""", (tuple(ids), tuple(ids), ))
|
||||
product_routes = cr.fetchall()
|
||||
old_proc = False
|
||||
key = tuple()
|
||||
key_routes = {}
|
||||
for proc, route in product_routes:
|
||||
key += (route,)
|
||||
if old_proc != proc:
|
||||
if key:
|
||||
if key_routes.get(key):
|
||||
key_routes[key] += [proc]
|
||||
else:
|
||||
key_routes[key] = [proc]
|
||||
old_proc = proc
|
||||
key = tuple()
|
||||
return key_routes
|
||||
|
||||
|
||||
def _get_wh_loc_dict(self, cr, uid, procurements, context=None):
|
||||
wh_dict = {}
|
||||
for procurement in procurements:
|
||||
if wh_dict.get(procurement.warehouse_id.id):
|
||||
if wh_dict[procurement.warehouse_id.id].get(procurement.location_id):
|
||||
wh_dict[procurement.warehouse_id.id][procurement.location_id] += [procurement]
|
||||
else:
|
||||
wh_dict[procurement.warehouse_id.id][procurement.location_id] = [procurement]
|
||||
else:
|
||||
wh_dict[procurement.warehouse_id.id] = {}
|
||||
wh_dict[procurement.warehouse_id.id][procurement.location_id] = [procurement]
|
||||
return wh_dict
|
||||
|
||||
|
||||
def _find_suitable_rule_multi(self, cr, uid, procurements, domain = [], context=None):
|
||||
'''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior'''
|
||||
results_dict = {}
|
||||
pull_obj = self.pool.get('procurement.rule')
|
||||
warehouse_route_ids = []
|
||||
for procurement in procurements: #Could be replaced by one query for all route_ids
|
||||
if procurement.route_ids:
|
||||
procurement_route_ids = [x.id for x in procurement.route_ids]
|
||||
loc = procurement.location_id
|
||||
loc_domain = [('location_id.parent_left', '<=', loc.parent_left),
|
||||
('location_id.parent_right', '>=', loc.parent_left)]
|
||||
if procurement.warehouse_id:
|
||||
domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)]
|
||||
res = pull_obj.search(cr, uid, loc_domain + [('route_id', 'in', procurement_route_ids)], order='route_sequence, sequence', context=context)
|
||||
if res and res[0]:
|
||||
results_dict[procurement.id] = res[0]
|
||||
|
||||
|
||||
procurements_to_check = [x for x in procurements if x.id not in results_dict.keys()]
|
||||
#group by warehouse_id:
|
||||
wh_dict = self._get_wh_loc_dict(cr, uid, procurements_to_check, context=context)
|
||||
for wh in wh_dict.keys():
|
||||
warehouse_route_ids = []
|
||||
domain = []
|
||||
check_wh = False
|
||||
for loc in wh_dict[wh].keys():
|
||||
procurement = wh_dict[wh][loc][0]
|
||||
loc_domain = [('location_id.parent_left', '<=', loc.parent_left),
|
||||
('location_id.parent_right', '>=', loc.parent_left)]
|
||||
if wh and not check_wh:
|
||||
domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)]
|
||||
warehouse_route_ids = [x.id for x in procurement.warehouse_id.route_ids]
|
||||
check_wh = True
|
||||
key_routes = self._get_route_group_dict(cr, uid, wh_dict[wh][loc], context=context)
|
||||
for key in key_routes.keys():
|
||||
procurements = self.browse(cr, uid, key_routes[key], context=context)
|
||||
domain = loc_domain + domain
|
||||
res = pull_obj.search(cr, uid, domain + [('route_id', 'in', list(key))], order='route_sequence, sequence', context=context)
|
||||
result = False
|
||||
if res and res[0]:
|
||||
result = res[0]
|
||||
elif warehouse_route_ids:
|
||||
res = pull_obj.search(cr, uid, domain + [('route_id', 'in', warehouse_route_ids)], order='route_sequence, sequence', context=context)
|
||||
result = res and res[0]
|
||||
if not result:
|
||||
res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context)
|
||||
result = res and res[0]
|
||||
for proc in key_routes[key]:
|
||||
results_dict[proc] = result
|
||||
return results_dict
|
||||
|
||||
|
||||
|
||||
def _search_suitable_rule(self, cr, uid, procurement, domain, context=None):
|
||||
'''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior'''
|
||||
pull_obj = self.pool.get('procurement.rule')
|
||||
@@ -325,13 +451,7 @@ class procurement_order(osv.osv):
|
||||
'group_id': orderpoint.group_id.id,
|
||||
}
|
||||
|
||||
def _product_virtual_get(self, cr, uid, order_point):
|
||||
product_obj = self.pool.get('product.product')
|
||||
return product_obj._product_available(cr, uid,
|
||||
[order_point.product_id.id],
|
||||
context={'location': order_point.location_id.id})[order_point.product_id.id]['virtual_available']
|
||||
|
||||
def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
|
||||
def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
|
||||
'''
|
||||
Create procurement based on Orderpoint
|
||||
|
||||
@@ -343,46 +463,79 @@ class procurement_order(osv.osv):
|
||||
if use_new_cursor:
|
||||
cr = openerp.registry(cr.dbname).cursor()
|
||||
orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
|
||||
|
||||
procurement_obj = self.pool.get('procurement.order')
|
||||
product_obj = self.pool.get('product.product')
|
||||
|
||||
dom = company_id and [('company_id', '=', company_id)] or []
|
||||
orderpoint_ids = orderpoint_obj.search(cr, uid, dom)
|
||||
orderpoint_ids = orderpoint_obj.search(cr, uid, dom, order="location_id")
|
||||
prev_ids = []
|
||||
tot_procs = []
|
||||
while orderpoint_ids:
|
||||
ids = orderpoint_ids[:100]
|
||||
del orderpoint_ids[:100]
|
||||
for op in orderpoint_obj.browse(cr, uid, ids, context=context):
|
||||
try:
|
||||
prods = self._product_virtual_get(cr, uid, op)
|
||||
if prods is None:
|
||||
continue
|
||||
if prods < op.product_min_qty:
|
||||
qty = max(op.product_min_qty, op.product_max_qty) - prods
|
||||
ids = orderpoint_ids[:1000]
|
||||
del orderpoint_ids[:1000]
|
||||
product_dict = {}
|
||||
ops_dict = {}
|
||||
ops = orderpoint_obj.browse(cr, uid, ids, context=context)
|
||||
|
||||
reste = op.qty_multiple > 0 and qty % op.qty_multiple or 0.0
|
||||
if reste > 0:
|
||||
qty += op.qty_multiple - reste
|
||||
#Calculate groups that can be executed together
|
||||
for op in ops:
|
||||
key = (op.location_id.id,)
|
||||
if not product_dict.get(key):
|
||||
product_dict[key] = [op.product_id]
|
||||
ops_dict[key] = [op]
|
||||
else:
|
||||
product_dict[key] += [op.product_id]
|
||||
ops_dict[key] += [op]
|
||||
|
||||
if qty <= 0:
|
||||
for key in product_dict.keys():
|
||||
ctx = context.copy()
|
||||
ctx.update({'location': ops_dict[key][0].location_id.id})
|
||||
prod_qty = product_obj._product_available(cr, uid, [x.id for x in product_dict[key]],
|
||||
context=ctx)
|
||||
subtract_qty = orderpoint_obj.subtract_procurements_from_orderpoints(cr, uid, [x.id for x in ops_dict[key]], context=context)
|
||||
for op in ops_dict[key]:
|
||||
try:
|
||||
prods = prod_qty[op.product_id.id]['virtual_available']
|
||||
if prods is None:
|
||||
continue
|
||||
if prods <= op.product_min_qty:
|
||||
qty = max(op.product_min_qty, op.product_max_qty) - prods
|
||||
reste = op.qty_multiple > 0 and qty % op.qty_multiple or 0.0
|
||||
if reste > 0:
|
||||
qty += op.qty_multiple - reste
|
||||
|
||||
qty -= orderpoint_obj.subtract_procurements(cr, uid, op, context=context)
|
||||
if qty < 0:
|
||||
continue
|
||||
|
||||
qty -= subtract_qty[op.id]
|
||||
|
||||
if qty >= 0:
|
||||
proc_id = procurement_obj.create(cr, uid,
|
||||
self._prepare_orderpoint_procurement(cr, uid, op, qty, context=context),
|
||||
context=context)
|
||||
tot_procs.append(proc_id)
|
||||
if use_new_cursor:
|
||||
cr.commit()
|
||||
except OperationalError:
|
||||
if use_new_cursor:
|
||||
orderpoint_ids.append(op.id)
|
||||
cr.rollback()
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
tot_procs.reverse()
|
||||
self.run(cr, uid, tot_procs, context=context)
|
||||
tot_procs = []
|
||||
if use_new_cursor:
|
||||
cr.commit()
|
||||
except OperationalError:
|
||||
if use_new_cursor:
|
||||
cr.rollback()
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
if qty > 0:
|
||||
proc_id = procurement_obj.create(cr, uid,
|
||||
self._prepare_orderpoint_procurement(cr, uid, op, qty, context=context),
|
||||
context=context)
|
||||
self.check(cr, uid, [proc_id])
|
||||
self.run(cr, uid, [proc_id])
|
||||
if use_new_cursor:
|
||||
cr.commit()
|
||||
except OperationalError:
|
||||
if use_new_cursor:
|
||||
orderpoint_ids.append(op.id)
|
||||
cr.rollback()
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
if use_new_cursor:
|
||||
cr.commit()
|
||||
if prev_ids == ids:
|
||||
@@ -393,4 +546,4 @@ class procurement_order(osv.osv):
|
||||
if use_new_cursor:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
return {}
|
||||
return {}
|
||||
+30
-13
@@ -4075,22 +4075,39 @@ class stock_warehouse_orderpoint(osv.osv):
|
||||
_name = "stock.warehouse.orderpoint"
|
||||
_description = "Minimum Inventory Rule"
|
||||
|
||||
def subtract_procurements(self, cr, uid, orderpoint, context=None):
|
||||
def subtract_procurements_from_orderpoints(self, cr, uid, orderpoint_ids, context=None):
|
||||
'''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it.
|
||||
'''
|
||||
qty = 0
|
||||
|
||||
cr.execute("""select op.id, p.id, p.product_uom, p.product_qty, pt.uom_id, sm.product_qty from procurement_order as p left join stock_move as sm ON sm.procurement_id = p.id,
|
||||
stock_warehouse_orderpoint op, product_product pp, product_template pt
|
||||
WHERE p.orderpoint_id = op.id AND p.state not in ('done', 'cancel') AND (sm.state IS NULL OR sm.state not in ('draft'))
|
||||
AND pp.id = p.product_id AND pp.product_tmpl_id = pt.id
|
||||
AND op.id IN %s
|
||||
ORDER BY op.id, p.id
|
||||
""", (tuple(orderpoint_ids),))
|
||||
results = cr.fetchall()
|
||||
current_proc = False
|
||||
current_op = False
|
||||
uom_obj = self.pool.get("product.uom")
|
||||
for procurement in orderpoint.procurement_ids:
|
||||
if procurement.state in ('cancel', 'done'):
|
||||
continue
|
||||
procurement_qty = uom_obj._compute_qty_obj(cr, uid, procurement.product_uom, procurement.product_qty, procurement.product_id.uom_id, context=context)
|
||||
for move in procurement.move_ids:
|
||||
#need to add the moves in draft as they aren't in the virtual quantity + moves that have not been created yet
|
||||
if move.state not in ('draft'):
|
||||
#if move is already confirmed, assigned or done, the virtual stock is already taking this into account so it shouldn't be deducted
|
||||
procurement_qty -= move.product_qty
|
||||
qty += procurement_qty
|
||||
return qty
|
||||
op_qty = 0
|
||||
res = dict.fromkeys(orderpoint_ids, 0.0)
|
||||
for move_result in results:
|
||||
op = move_result[0]
|
||||
if current_op != op:
|
||||
if current_op:
|
||||
res[current_op] = op_qty
|
||||
current_op = op
|
||||
op_qty = 0
|
||||
proc = move_result[1]
|
||||
if proc != current_proc:
|
||||
op_qty += uom_obj._compute_qty(cr, uid, move_result[2], move_result[3], move_result[4], round=False)
|
||||
current_proc = proc
|
||||
if move_result[5]: #If a move is associated (is move qty)
|
||||
op_qty -= move_result[5]
|
||||
if current_op:
|
||||
res[current_op] = op_qty
|
||||
return res
|
||||
|
||||
def _check_product_uom(self, cr, uid, ids, context=None):
|
||||
'''
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
-
|
||||
!python {model: stock.quant}: |
|
||||
quant_ids = self.search(cr, uid, [('location_id', '=', ref('stock.stock_location_customers')),('qty', '=', 200), ('product_id', '=', ref("drop_shop_product"))])
|
||||
print quant_ids, [(x.qty, x.history_ids) for x in self.browse(cr, uid, quant_ids)]
|
||||
assert quant_ids, 'No Quant found'
|
||||
assert len(quant_ids) == 1
|
||||
assert len(self.browse(cr, uid, quant_ids)[0].history_ids) == 1
|
||||
assert len(quant_ids) == 1, 'There should be exactly one quant'
|
||||
assert len(self.browse(cr, uid, quant_ids)[0].history_ids) == 1, 'The quant should have exactly 1 move in its history'
|
||||
Reference in New Issue
Block a user