diff --git a/__init__.py b/__init__.py index e4717626b1..bb58ef5f7b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,24 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. + +from trytond.pool import Pool from .order_point import * from .product import * from .purchase_request import * from .shipment import * + + +def register(): + Pool.register( + OrderPoint, + Product, + PurchaseRequest, + CreatePurchaseRequestStart, + CreatePurchaseAskTerm, + CreatePurchaseAskParty, + ShipmentInternal, + module='stock_supply', type_='model') + Pool.register( + CreatePurchaseRequest, + CreatePurchase, + module='stock_supply', type_='wizard') diff --git a/order_point.py b/order_point.py index 1140d5de75..59c8cf8d69 100644 --- a/order_point.py +++ b/order_point.py @@ -3,19 +3,19 @@ from trytond.model import ModelView, ModelSQL, fields from trytond.pyson import If, Equal, Eval, Not, In, Get from trytond.transaction import Transaction -from trytond.pool import Pool from trytond.backend import TableHandler +__all__ = ['OrderPoint'] + class OrderPoint(ModelSQL, ModelView): """ - Order Point: Provide a way to define a supply policy for each + Order Point + Provide a way to define a supply policy for each product on each locations. Order points on warehouse are considered by the supply scheduler to generate purchase requests. """ - _name = 'stock.order_point' - _description = "Order Point" - + __name__ = 'stock.order_point' product = fields.Many2One('product.product', 'Product', required=True, select=True, domain=[ @@ -69,18 +69,19 @@ class OrderPoint(ModelSQL, ModelView): unit_digits = fields.Function(fields.Integer('Unit Digits'), 'get_unit_digits') - def __init__(self): - super(OrderPoint, self).__init__() - self._constraints += [ + @classmethod + def __setup__(cls): + super(OrderPoint, cls).__setup__() + cls._constraints += [ ('check_concurrent_internal', 'concurrent_internal_op'), ('check_uniqueness', 'unique_op'), ] - self._sql_constraints += [ + cls._sql_constraints += [ ('check_max_qty_greater_min_qty', 'CHECK(max_quantity >= min_quantity)', 'Maximal quantity must be bigger than Minimal quantity'), ] - self._error_messages.update({ + cls._error_messages.update({ 'unique_op': 'Only one order point is allowed '\ 'for each product-location pair.', 'concurrent_internal_op': 'You can not define ' \ @@ -88,68 +89,63 @@ class OrderPoint(ModelSQL, ModelView): 'with opposite locations.', }) - def init(self, module_name): + @classmethod + def __register__(cls, module_name): cursor = Transaction().cursor # Migration from 2.2 - table = TableHandler(cursor, self, module_name) + table = TableHandler(cursor, cls, module_name) table.drop_constraint('check_min_max_quantity') - super(OrderPoint, self).init(module_name) + super(OrderPoint, cls).__register__(module_name) - def default_type(self): + @staticmethod + def default_type(): return "purchase" - def on_change_product(self, vals): - product_obj = Pool().get('product.product') + def on_change_product(self): res = { 'unit': None, 'unit.rec_name': '', 'unit_digits': 2, - } - if vals.get('product'): - product = product_obj.browse(vals['product']) - res['unit'] = product.default_uom.id - res['unit.rec_name'] = product.default_uom.rec_name - res['unit_digits'] = product.default_uom.digits + } + if self.product: + res['unit'] = self.product.default_uom.id + res['unit.rec_name'] = self.product.default_uom.rec_name + res['unit_digits'] = self.product.default_uom.digits return res - def get_unit(self, ids, name): - res = {} - for order in self.browse(ids): - res[order.id] = order.product.default_uom.id - return res + def get_unit(self, name): + return self.product.default_uom.id - def get_unit_digits(self, ids, name): - res = {} - for order in self.browse(ids): - res[order.id] = order.product.default_uom.digits - return res + def get_unit_digits(self, name): + return self.product.default_uom.digits - def check_concurrent_internal(self, ids): + @classmethod + def check_concurrent_internal(cls, orders): """ Ensure that there is no 'concurrent' internal order points. I.E. no two order point with opposite location for the same product and same company. """ - internal_ids = self.search([ - ('id', 'in', ids), - ('type', '=', 'internal'), - ]) - if not internal_ids: + internals = cls.search([ + ('id', 'in', [o.id for o in orders]), + ('type', '=', 'internal'), + ]) + if not internals: return True query = ['OR'] - for op in self.browse(internal_ids): + for op in internals: arg = ['AND', ('provisioning_location', '=', op.storage_location.id), ('storage_location', '=', op.provisioning_location.id), ('company', '=', op.company.id), ('type', '=', 'internal')] query.append(arg) - ids = self.search(query) - return not bool(ids) + return not bool(cls.search(query)) - def _type2field(self, type=None): + @staticmethod + def _type2field(type=None): t2f = { 'purchase': 'warehouse_location', 'internal': 'storage_location', @@ -159,36 +155,30 @@ class OrderPoint(ModelSQL, ModelView): else: return t2f[type] - def check_uniqueness(self, ids): + @classmethod + def check_uniqueness(cls, orders): """ Ensure uniqueness of order points. I.E that there is no several order point for the same location, the same product and the same company. """ query = ['OR'] - for op in self.browse(ids): - field = self._type2field(op.type) + for op in orders: + field = cls._type2field(op.type) arg = ['AND', ('product', '=', op.product.id), - (field, '=', op[field].id), + (field, '=', getattr(op, field).id), ('id', '!=', op.id), ('company', '=', op.company.id), ] query.append(arg) - ids = self.search(query) - return not bool(ids) + return not bool(cls.search(query)) - def get_rec_name(self, ids, name): - if not ids: - return {} - if isinstance(ids, (int, long)): - ids = [ids] - res = {} - for op in self.browse(ids): - res[op.id] = "%s@%s" % (op.product.name, op.location.name) - return res + def get_rec_name(self, name): + return "%s@%s" % (self.product.name, self.location.name) - def search_rec_name(self, name, clause): + @classmethod + def search_rec_name(cls, name, clause): res = [] names = clause[2].split('@', 1) res.append(('product.template.name', clause[1], names[0])) @@ -196,27 +186,22 @@ class OrderPoint(ModelSQL, ModelView): res.append(('location', clause[1], names[1])) return res - def get_location(self, ids, name): - res = {} - for op in self.browse(ids): - if op.type == 'purchase': - res[op.id] = op.warehouse_location.id - elif op.type == 'internal': - res[op.id] = op.storage_location.id - else: - res[op.id] = None - return res + def get_location(self, name): + if self.type == 'purchase': + return self.warehouse_location.id + elif self.type == 'internal': + return self.storage_location.id - def search_location(self, name, domain=None): + @classmethod + def search_location(cls, name, domain=None): ids = [] - for type, field in self._type2field().iteritems(): + for type, field in cls._type2field().iteritems(): args = [('type', '=', type)] for _, operator, operand in domain: args.append((field, operator, operand)) - ids.extend(self.search(args)) + ids.extend([o.id for o in cls.search(args)]) return [('id', 'in', ids)] - def default_company(self): + @staticmethod + def default_company(): return Transaction().context.get('company') - -OrderPoint() diff --git a/product.py b/product.py index 7b935b88bd..f259f0bc13 100644 --- a/product.py +++ b/product.py @@ -1,12 +1,13 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -from trytond.model import ModelView, ModelSQL, fields +from trytond.model import fields +from trytond.pool import PoolMeta + +__all__ = ['Product'] +__metaclass__ = PoolMeta -class Product(ModelSQL, ModelView): - _name = "product.product" - +class Product: + __name__ = "product.product" order_points = fields.One2Many( 'stock.order_point', 'product', 'Order Points') - -Product() diff --git a/purchase_request.py b/purchase_request.py index 7af55a2839..7a4ce1607d 100644 --- a/purchase_request.py +++ b/purchase_request.py @@ -12,12 +12,14 @@ from trytond.pyson import If, In, Eval, Get from trytond.transaction import Transaction from trytond.pool import Pool +__all__ = ['PurchaseRequest', + 'CreatePurchaseRequestStart', 'CreatePurchaseRequest', + 'CreatePurchaseAskTerm', 'CreatePurchaseAskParty', 'CreatePurchase'] + class PurchaseRequest(ModelSQL, ModelView): 'Purchase Request' - _name = 'purchase.request' - _description = __doc__ - + __name__ = 'purchase.request' product = fields.Many2One('product.product', 'Product', required=True, select=True, readonly=True, domain=[('purchasable', '=', True)]) party = fields.Many2One('party.party', 'Party', select=True) @@ -56,39 +58,37 @@ class PurchaseRequest(ModelSQL, ModelView): ('cancel', 'Cancel'), ], 'State'), 'get_state') - def __init__(self): - super(PurchaseRequest, self).__init__() - self._order[0] = ('id', 'DESC') - self._error_messages.update({ + @classmethod + def __setup__(cls): + super(PurchaseRequest, cls).__setup__() + cls._order[0] = ('id', 'DESC') + cls._error_messages.update({ 'create_request': 'Purchase requests are only created ' \ 'by the system.', }) - self._sql_constraints += [ + cls._sql_constraints += [ ('check_purchase_request_quantity', 'CHECK(quantity > 0)', 'The requested quantity must be greater than 0'), ] - def init(self, module_name): + @classmethod + def __register__(cls, module_name): cursor = Transaction().cursor - super(PurchaseRequest, self).init(module_name) + super(PurchaseRequest, cls).__register__(module_name) # Migration from 2.0: empty order point origin is -1 instead of 0 cursor.execute('UPDATE "%s" ' - 'SET origin = %%s WHERE origin = %%s' % self._table, + 'SET origin = %%s WHERE origin = %%s' % cls._table, ('stock.order_point,-1', 'stock.order_point,0')) - def get_rec_name(self, ids, name): - if isinstance(ids, (int, long)): - ids = [ids] - res = {} - for pr in self.browse(ids): - if pr.warehouse: - res[pr.id] = "%s@%s" % (pr.product.name, pr.warehouse.name) - else: - res[pr.id] = pr.product.name - return res + def get_rec_name(self, name): + if self.warehouse: + return "%s@%s" % (self.product.name, self.warehouse.name) + else: + return self.product.name - def search_rec_name(self, name, clause): + @classmethod + def search_rec_name(cls, name, clause): res = [] names = clause[2].split('@', 1) res.append(('product.template.name', clause[1], names[0])) @@ -96,86 +96,79 @@ class PurchaseRequest(ModelSQL, ModelView): res.append(('warehouse', clause[1], names[1])) return res - def default_company(self): + @staticmethod + def default_company(): return Transaction().context.get('company') - def get_purchase(self, ids, name): - res = {} + def get_purchase(self, name): + if self.purchase_line: + return self.purchase_line.purchase.id - requests = self.browse(ids) - for request in requests: - if request.purchase_line: - res[request.id] = request.purchase_line.purchase.id + def get_state(self, name): + if self.purchase_line: + if self.purchase_line.purchase.state == 'cancel': + return 'cancel' + elif self.purchase_line.purchase.state == 'done': + return 'done' else: - res[request.id] = None - return res + return 'purchased' + return 'draft' - def get_state(self, ids, name): - res = {}.fromkeys(ids, 'draft') - for request in self.browse(ids): - if request.purchase_line: - if request.purchase_line.purchase.state == 'cancel': - res[request.id] = 'cancel' - elif request.purchase_line.purchase.state == 'done': - res[request.id] = 'done' - else: - res[request.id] = 'purchased' - return res + def get_warehouse_required(self, name): + return self.product.type in ('goods', 'assets') - def get_warehouse_required(self, ids, name): - requireds = {} - for request in self.browse(ids): - requireds[request.id] = request.product.type in ('goods', 'assets') - return requireds - - def origin_get(self): - model_obj = Pool().get('ir.model') + @staticmethod + def origin_get(): + Model = Pool().get('ir.model') res = [] - model_ids = model_obj.search([ - ('model', '=', 'stock.order_point'), - ]) - for model in model_obj.browse(model_ids): + models = Model.search([ + ('model', '=', 'stock.order_point'), + ]) + for model in models: res.append([model.model, model.name]) return res - def generate_requests(self): + @classmethod + def generate_requests(cls): """ For each product compute the purchase request that must be create today to meet product outputs. """ pool = Pool() - order_point_obj = pool.get('stock.order_point') - product_obj = pool.get('product.product') - location_obj = pool.get('stock.location') - user_obj = pool.get('res.user') - company = user_obj.browse(Transaction().user).company + OrderPoint = pool.get('stock.order_point') + Product = pool.get('product.product') + Location = pool.get('stock.location') + User = pool.get('res.user') + company = User(Transaction().user).company # fetch warehouses: - warehouse_ids = location_obj.search([ + warehouses = Location.search([ ('type', '=', 'warehouse'), ]) + warehouse_ids = [w.id for w in warehouses] # fetch order points - order_point_ids = order_point_obj.search([ + order_points = OrderPoint.search([ ('type', '=', 'purchase'), ]) # index them by product product2ops = {} - for order_point in order_point_obj.browse(order_point_ids): + for order_point in order_points: product2ops[ (order_point.warehouse_location.id, order_point.product.id) ] = order_point # fetch goods and assets # ordered by ids to speedup reduce_ids in products_by_location - product_ids = product_obj.search([ + products = Product.search([ ('type', 'in', ['goods', 'assets']), ('consumable', '=', False), ('purchasable', '=', True), ], order=[('id', 'ASC')]) + product_ids = [p.id for p in products] #aggregate product by minimum supply date date2products = {} - for product in product_obj.browse(product_ids): - min_date, max_date = self.get_supply_dates(product) + for product in products: + min_date, max_date = cls.get_supply_dates(product) date2products.setdefault((min_date, max_date), []).append(product) # compute requests @@ -187,13 +180,13 @@ class PurchaseRequest(ModelSQL, ModelView): product_ids = [p.id for p in products[i:i + cursor.IN_MAX]] with Transaction().set_context(forecast=True, stock_date_end=min_date or datetime.date.max): - pbl = product_obj.products_by_location(warehouse_ids, + pbl = Product.products_by_location(warehouse_ids, product_ids, with_childs=True, skip_zero=False) for warehouse_id in warehouse_ids: min_date_qties = dict((x, pbl.pop((warehouse_id, x))) for x in product_ids) # Search for shortage between min-max - shortages = self.get_shortage(warehouse_id, product_ids, + shortages = cls.get_shortage(warehouse_id, product_ids, min_date, max_date, min_date_qties=min_date_qties, order_points=product2ops) @@ -204,53 +197,44 @@ class PurchaseRequest(ModelSQL, ModelView): order_point = product2ops.get( (warehouse_id, product.id)) # generate request values - request_val = self.compute_request(product, + request = cls.compute_request(product, warehouse_id, shortage_date, product_quantity, company, order_point) - new_requests.append(request_val) + new_requests.append(request) - new_requests = self.compare_requests(new_requests) + new_requests = cls.compare_requests(new_requests) - self.create_requests(new_requests) - return {} - - def create_requests(self, new_requests): - request_obj = Pool().get('purchase.request') + cls.create_requests(new_requests) + @classmethod + def create_requests(cls, new_requests): for new_req in new_requests: - if new_req['supply_date'] == datetime.date.max: - new_req['supply_date'] = None - if new_req['quantity'] > 0.0: - new_req.update({ - 'product': new_req['product'].id, - 'party': new_req['party'] and new_req['party'].id, - 'uom': new_req['uom'].id, - 'computed_uom': new_req['computed_uom'].id, - 'company': new_req['company'].id - }) - request_obj.create(new_req) + if new_req.supply_date == datetime.date.max: + new_req.supply_date = None + if new_req.quantity > 0.0: + new_req.save() - def compare_requests(self, new_requests): + @classmethod + def compare_requests(cls, new_requests): """ Compare new_requests with already existing request to avoid to re-create existing requests. """ # delete purchase request without a purchase line pool = Pool() - uom_obj = pool.get('product.uom') - request_obj = pool.get('purchase.request') - req_ids = request_obj.search([ + Uom = pool.get('product.uom') + Request = pool.get('purchase.request') + reqs = Request.search([ ('purchase_line', '=', None), ('origin', 'like', 'stock.order_point,%'), ]) - request_obj.delete(req_ids) + Request.delete(reqs) - req_ids = request_obj.search([ + requests = Request.search([ ('purchase_line.moves', '=', None), ('purchase_line.purchase.state', '!=', 'cancel'), ('origin', 'like', 'stock.order_point,%'), ]) - requests = request_obj.browse(req_ids) # Fetch data from existing requests existing_req = {} for request in requests: @@ -260,7 +244,7 @@ class PurchaseRequest(ModelSQL, ModelView): request.warehouse.id != pline.purchase.warehouse.id: continue # Take smallest amount between request and purchase line - req_qty = uom_obj.compute_qty(request.uom, request.quantity, + req_qty = Uom.compute_qty(request.uom, request.quantity, pline.unit) if req_qty < pline.quantity: quantity = request.quantity @@ -282,46 +266,40 @@ class PurchaseRequest(ModelSQL, ModelView): i.sort(lambda r, s: cmp(r['supply_date'], s['supply_date'])) # Update new requests to take existing requests into account - new_requests.sort(key=operator.itemgetter('supply_date')) + new_requests.sort(key=operator.attrgetter('supply_date')) for new_req in new_requests: - for old_req in existing_req.get((new_req['product'].id, - new_req['warehouse']), []): - if old_req['supply_date'] <= new_req['supply_date']: - quantity = uom_obj.compute_qty(old_req['uom'], - old_req['quantity'], new_req['uom']) - new_req['quantity'] = max(0.0, - new_req['quantity'] - quantity) - new_req['computed_quantity'] = new_req['quantity'] - old_req['quantity'] = uom_obj.compute_qty(new_req['uom'], - max(0.0, quantity - new_req['quantity']), - old_req['uom']) + for old_req in existing_req.get( + (new_req.product.id, new_req.warehouse.id), []): + if old_req['supply_date'] <= new_req.supply_date: + quantity = Uom.compute_qty(old_req['uom'], + old_req['quantity'], new_req.uom) + new_req.quantity = max(0.0, new_req.quantity - quantity) + new_req.computed_quantity = new_req.quantity + old_req['quantity'] = Uom.compute_qty(new_req.uom, + max(0.0, quantity - new_req.quantity), old_req['uom']) else: break return new_requests - def get_supply_dates(self, product): + @classmethod + def get_supply_dates(cls, product): """ Return the minimal interval of earliest supply dates for a product. - - :param product: a BrowseRecord of the Product - :return: a tuple with the two dates """ - product_supplier_obj = Pool().get('purchase.product_supplier') - date_obj = Pool().get('ir.date') + Date = Pool().get('ir.date') min_date = None max_date = None - today = date_obj.today() + today = Date.today() for product_supplier in product.product_suppliers: - supply_date = product_supplier_obj.compute_supply_date( - product_supplier, date=today) + supply_date = product_supplier.compute_supply_date(date=today) # TODO next_day is by default today + 1 but should depends # on the CRON activity next_day = today + datetime.timedelta(1) - next_supply_date = product_supplier_obj.compute_supply_date( - product_supplier, date=next_day) + next_supply_date = product_supplier.compute_supply_date( + date=next_day) if (not min_date) or supply_date < min_date: min_date = supply_date if (not max_date): @@ -337,20 +315,18 @@ class PurchaseRequest(ModelSQL, ModelView): return (min_date, max_date) - def find_best_supplier(self, product, date): + @classmethod + def find_best_supplier(cls, product, date): ''' Return the best supplier and purchase_date for the product. ''' - pool = Pool() - date_obj = pool.get('ir.date') - product_supplier_obj = pool.get('purchase.product_supplier') + Date = Pool().get('ir.date') supplier = None timedelta = datetime.timedelta.max - today = date_obj.today() + today = Date.today() for product_supplier in product.product_suppliers: - supply_date = product_supplier_obj.compute_supply_date( - product_supplier, date=today) + supply_date = product_supplier.compute_supply_date(date=today) sup_timedelta = date - supply_date if not supplier: supplier = product_supplier.party @@ -364,13 +340,13 @@ class PurchaseRequest(ModelSQL, ModelView): timedelta = sup_timedelta if supplier: - purchase_date = product_supplier_obj.compute_purchase_date( - product_supplier, date) + purchase_date = product_supplier.compute_purchase_date(date) else: purchase_date = today return supplier, purchase_date - def compute_request(self, product, location_id, shortage_date, + @classmethod + def compute_request(cls, product, location_id, shortage_date, product_quantity, company, order_point=None): """ Return the value of the purchase request which will answer to @@ -379,13 +355,14 @@ class PurchaseRequest(ModelSQL, ModelView): supplier. """ pool = Pool() - uom_obj = pool.get('product.uom') + Uom = pool.get('product.uom') + Request = pool.get('purchase.request') - supplier, purchase_date = self.find_best_supplier(product, + supplier, purchase_date = cls.find_best_supplier(product, shortage_date) max_quantity = order_point and order_point.max_quantity or 0.0 - quantity = uom_obj.compute_qty(product.default_uom, + quantity = Uom.compute_qty(product.default_uom, max_quantity - product_quantity, product.purchase_uom or product.default_uom) @@ -393,21 +370,22 @@ class PurchaseRequest(ModelSQL, ModelView): origin = 'stock.order_point,%s' % order_point.id else: origin = 'stock.order_point,-1' - return {'product': product, - 'party': supplier and supplier or None, - 'quantity': quantity, - 'uom': product.purchase_uom or product.default_uom, - 'computed_quantity': quantity, - 'computed_uom': product.purchase_uom or product.default_uom, - 'purchase_date': purchase_date, - 'supply_date': shortage_date, - 'stock_level': product_quantity, - 'company': company, - 'warehouse': location_id, - 'origin': origin, - } + return Request(product=product, + party=supplier and supplier or None, + quantity=quantity, + uom=product.purchase_uom or product.default_uom, + computed_quantity=quantity, + computed_uom=product.purchase_uom or product.default_uom, + purchase_date=purchase_date, + supply_date=shortage_date, + stock_level=product_quantity, + company=company, + warehouse=location_id, + origin=origin, + ) - def get_shortage(self, location_id, product_ids, min_date, max_date, + @classmethod + def get_shortage(cls, location_id, product_ids, min_date, max_date, min_date_qties, order_points): """ Return for each product the first date between min_date and max_date @@ -420,7 +398,7 @@ class PurchaseRequest(ModelSQL, ModelView): min_date_qty is the quantities for each products at the min_date. order_points is a dictionary that links products to order point. """ - product_obj = Pool().get('product.product') + Product = Pool().get('product.product') res_dates = {} res_qties = {} @@ -449,7 +427,7 @@ class PurchaseRequest(ModelSQL, ModelView): with Transaction().set_context(stock_date_start=current_date, stock_date_end=current_date): - pbl = product_obj.products_by_location([location_id], + pbl = Product.products_by_location([location_id], product_ids, with_childs=True, skip_zero=False) for key, qty in pbl.iteritems(): _, product_id = key @@ -461,27 +439,22 @@ class PurchaseRequest(ModelSQL, ModelView): return dict((x, (res_dates.get(x), res_qties.get(x))) for x in product_ids) - def create(self, vals): + @classmethod + def create(cls, vals): for field_name in ('product', 'quantity', 'uom', 'company'): if not vals.get(field_name): - self.raise_user_error('create_request') - return super(PurchaseRequest, self).create(vals) - -PurchaseRequest() + cls.raise_user_error('create_request') + return super(PurchaseRequest, cls).create(vals) class CreatePurchaseRequestStart(ModelView): 'Create Purchase Request' - _name = 'purchase.request.create.start' - _description = __doc__ - -CreatePurchaseRequestStart() + __name__ = 'purchase.request.create.start' class CreatePurchaseRequest(Wizard): 'Create Purchase Request' - _name = 'purchase.request.create' - + __name__ = 'purchase.request.create' start = StateView('purchase.request.create.start', 'stock_supply.purchase_request_create_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), @@ -489,44 +462,35 @@ class CreatePurchaseRequest(Wizard): ]) create_ = StateAction('stock_supply.act_purchase_request_form_draft') - def do_create_(self, session, action): - purchase_request_obj = Pool().get('purchase.request') - purchase_request_obj.generate_requests() + def do_create_(self, action): + PurchaseRequest = Pool().get('purchase.request') + PurchaseRequest.generate_requests() return action, {} - def transition_create_(self, session): + def transition_create_(self): return 'end' -CreatePurchaseRequest() - class CreatePurchaseAskTerm(ModelView): 'Create Purchase Ask Term' - _name = 'purchase.request.create_purchase.ask_term' - _description = __doc__ + __name__ = 'purchase.request.create_purchase.ask_term' party = fields.Many2One('party.party', 'Supplier', readonly=True) company = fields.Many2One('company.company', 'Company', readonly=True) payment_term = fields.Many2One( 'account.invoice.payment_term', 'Payment Term', required=True) -CreatePurchaseAskTerm() - class CreatePurchaseAskParty(ModelView): 'Create Purchase Ask Party' - _name = 'purchase.request.create_purchase.ask_party' - _description = __doc__ + __name__ = 'purchase.request.create_purchase.ask_party' product = fields.Many2One('product.product', 'Product', readonly=True) company = fields.Many2One('company.company', 'Company', readonly=True) party = fields.Many2One('party.party', 'Supplier', required=True) -CreatePurchaseAskParty() - class CreatePurchase(Wizard): 'Create Purchase' - _name = 'purchase.request.create_purchase' - + __name__ = 'purchase.request.create_purchase' start = StateTransition() ask_party = StateView('purchase.request.create_purchase.ask_party', 'stock_supply.purchase_request_create_purchase_ask_party', [ @@ -539,17 +503,18 @@ class CreatePurchase(Wizard): Button('Continue', 'start', 'tryton-go-next', default=True), ]) - def __init__(self): - super(CreatePurchase, self).__init__() - self._error_messages.update({ + @classmethod + def __setup__(cls): + super(CreatePurchase, cls).__setup__() + cls._error_messages.update({ 'missing_price': 'Purchase price is missing for product: %s ' \ '(id: %s)!', 'please_update': 'This price is necessary for creating purchase.' }) - def default_ask_party(self, session, fields): - request_obj = Pool().get('purchase.request') - requests = request_obj.browse(Transaction().context['active_ids']) + def default_ask_party(self, fields): + Request = Pool().get('purchase.request') + requests = Request.browse(Transaction().context['active_ids']) for request in requests: if request.purchase_line: continue @@ -563,9 +528,9 @@ class CreatePurchase(Wizard): 'company': request.company.id, } - def default_ask_term(self, session, fields): - request_obj = Pool().get('purchase.request') - requests = request_obj.browse(Transaction().context['active_ids']) + def default_ask_term(self, fields): + Request = Pool().get('purchase.request') + requests = Request.browse(Transaction().context['active_ids']) for request in requests: if (not request.party) or request.purchase_line: continue @@ -579,75 +544,68 @@ class CreatePurchase(Wizard): 'company': request.company.id, } - def _group_purchase_key(self, requests, request): + @staticmethod + def _group_purchase_key(requests, request): ''' The key to group lines by purchases - - :param requests: a list of requests - :param request: the request - - :return: a list of key-value as tuples of the purchase + A list of key-value as tuples of the purchase ''' - party_obj = Pool().get('party.party') - return ( - ('company', request.company.id), - ('party', request.party.id), - ('payment_term', request.party.supplier_payment_term.id), - ('warehouse', request.warehouse.id), + ('company', request.company), + ('party', request.party), + ('payment_term', request.party.supplier_payment_term), + ('warehouse', request.warehouse), # XXX use function field - ('currency', request.company.currency.id), - ('invoice_address', party_obj.address_get(request.party.id, - type='invoice')), + ('currency', request.company.currency), + ('invoice_address', request.party.address_get(type='invoice')), ) - def transition_start(self, session): + def transition_start(self): pool = Pool() - request_obj = pool.get('purchase.request') - party_obj = pool.get('party.party') - purchase_obj = pool.get('purchase.purchase') - line_obj = pool.get('purchase.line') - date_obj = pool.get('ir.date') + Request = pool.get('purchase.request') + Party = pool.get('party.party') + Purchase = pool.get('purchase.purchase') + Date = pool.get('ir.date') request_ids = Transaction().context['active_ids'] - if (session.ask_party.product - and session.ask_party.party - and session.ask_party.company): - req_ids = request_obj.search([ + if (getattr(self.ask_party, 'product', None) + and getattr(self.ask_party, 'party', None) + and getattr(self.ask_party, 'company', None)): + reqs = Request.search([ ('id', 'in', request_ids), ('party', '=', None), ]) - if req_ids: - request_obj.write(req_ids, { - 'party': session.ask_party.party.id, + if reqs: + Request.write(reqs, { + 'party': self.ask_party.party.id, }) - session.ask_party.product = None - session.ask_party.party = None - session.ask_party.company = None - elif (session.ask_term.payment_term - and session.ask_term.party - and session.ask_term.company): + self.ask_party.product = None + self.ask_party.party = None + self.ask_party.company = None + elif (getattr(self.ask_term, 'payment_term', None) + and getattr(self.ask_term, 'party', None) + and getattr(self.ask_term, 'company', None)): with Transaction().set_context( - company=session.ask_term.company.id): - party_obj.write(session.ask_term.party.id, { + company=self.ask_term.company.id): + Party.write([self.ask_term.party], { 'supplier_payment_term': \ - session.ask_term.payment_term.id, + self.ask_term.payment_term.id, }) - session.ask_term.payment_term = None - session.ask_term.party = None - session.ask_term.company = None + self.ask_term.payment_term = None + self.ask_term.party = None + self.ask_term.company = None - req_ids = request_obj.search([ + reqs = Request.search([ ('id', 'in', request_ids), ('purchase_line', '=', None), ('party', '=', None), ]) - if req_ids: + if reqs: return 'ask_party' - today = date_obj.today() - requests = request_obj.browse(request_ids) + today = Date.today() + requests = Request.browse(request_ids) requests = [r for r in requests if not r.purchase_line] if any(r for r in requests if not r.party.supplier_payment_term): @@ -667,68 +625,61 @@ class CreatePurchase(Wizard): purchase_date = today if purchase_date < today: purchase_date = today - values = { - 'purchase_date': purchase_date, - } - values.update(dict(key)) - purchase_id = purchase_obj.create(values) + purchase = Purchase(purchase_date=purchase_date) + for f, v in key: + setattr(purchase, f, v) + purchase.save() for request in grouped_requests: - values = self.compute_purchase_line(request) - values['purchase'] = purchase_id - line_id = line_obj.create(values) - request_obj.write(request.id, { - 'purchase_line': line_id, + line = self.compute_purchase_line(request) + line.purchase = purchase + line.save() + Request.write([request], { + 'purchase_line': line.id, }) return 'end' - def _get_tax_rule_pattern(self, request): + @staticmethod + def _get_tax_rule_pattern(request): ''' Get tax rule pattern - - :param request: the BrowseRecord of the purchase request - :return: a dictionary to use as pattern for tax rule ''' - res = {} - return res + return {} - def compute_purchase_line(self, request): + @classmethod + def compute_purchase_line(cls, request): pool = Pool() - product_obj = pool.get('product.product') - tax_rule_obj = pool.get('account.tax.rule') - line_obj = pool.get('purchase.line') + Product = pool.get('product.product') + Line = pool.get('purchase.line') - line = { - 'product': request.product.id, - 'unit': request.uom.id, - 'quantity': request.quantity, - 'description': request.product.name, - } + line = Line( + product=request.product, + unit=request.uom, + quantity=request.quantity, + description=request.product.name, + ) # XXX purchase with several lines of the same product with Transaction().set_context(uom=request.uom.id, supplier=request.party.id, currency=request.company.currency.id): - product_price = product_obj.get_purchase_price( - [request.product.id], request.quantity)[request.product.id] + product_price = Product.get_purchase_price( + [request.product], request.quantity)[request.product.id] product_price = product_price.quantize( - Decimal(1) / 10 ** line_obj.unit_price.digits[1]) + Decimal(1) / 10 ** Line.unit_price.digits[1]) if product_price is None: - self.raise_user_error('missing_price', (request.product.name, + cls.raise_user_error('missing_price', (request.product.name, request.product.id), 'please_update') - line['unit_price'] = product_price + line.unit_price = product_price taxes = [] for tax in request.product.supplier_taxes_used: if request.party and request.party.supplier_tax_rule: - pattern = self._get_tax_rule_pattern(request) - tax_ids = tax_rule_obj.apply(request.party.supplier_tax_rule, - tax, pattern) + pattern = cls._get_tax_rule_pattern(request) + tax_ids = request.party.supplier_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax.id) - line['taxes'] = [('add', taxes)] + line.taxes = taxes return line - -CreatePurchase() diff --git a/shipment.py b/shipment.py index c726e3226e..a9d5f7f593 100644 --- a/shipment.py +++ b/shipment.py @@ -2,13 +2,17 @@ #this repository contains the full copyright notices and license terms. from trytond.model import ModelView, ModelSQL from trytond.transaction import Transaction -from trytond.pool import Pool +from trytond.pool import Pool, PoolMeta + +__all__ = ['ShipmentInternal'] +__metaclass__ = PoolMeta class ShipmentInternal(ModelSQL, ModelView): - _name = 'stock.shipment.internal' + __name__ = 'stock.shipment.internal' - def init(self, module_name): + @classmethod + def __register__(cls, module_name): cursor = Transaction().cursor # Migration from 1.2: packing renamed into shipment cursor.execute("UPDATE ir_model_data "\ @@ -19,26 +23,27 @@ class ShipmentInternal(ModelSQL, ModelView): "SET model = REPLACE(model, 'packing', 'shipment') "\ "WHERE model like '%%packing%%' AND module = %s", (module_name,)) - super(ShipmentInternal, self).init(module_name) + super(ShipmentInternal, cls).__register__(module_name) - def generate_internal_shipment(self): + @classmethod + def generate_internal_shipment(cls): """ Generate internal shipments to meet order points defined on non-warehouse location. """ pool = Pool() - order_point_obj = pool.get('stock.order_point') - uom_obj = pool.get('product.uom') - product_obj = pool.get('product.product') - date_obj = pool.get('ir.date') - user_obj = pool.get('res.user') - user_record = user_obj.browse(Transaction().user) - today = date_obj.today() + OrderPoint = pool.get('stock.order_point') + Uom = pool.get('product.uom') + Product = pool.get('product.product') + Date = pool.get('ir.date') + User = pool.get('res.user') + Move = pool.get('stock.move') + user_record = User(Transaction().user) + today = Date.today() # fetch quantities on order points - op_ids = order_point_obj.search([ + order_points = OrderPoint.search([ ('type', '=', 'internal'), ]) - order_points = order_point_obj.browse(op_ids) id2product = {} location_ids = [] for op in order_points: @@ -46,7 +51,7 @@ class ShipmentInternal(ModelSQL, ModelView): location_ids.append(op.storage_location.id) with Transaction().set_context(stock_date_end=today): - pbl = product_obj.products_by_location(location_ids, + pbl = Product.products_by_location(location_ids, list(id2product.iterkeys()), with_childs=True) # Create a list of move to create @@ -60,21 +65,21 @@ class ShipmentInternal(ModelSQL, ModelView): moves[key] = op.max_quantity - qty # Compare with existing draft shipments - shipment_ids = self.search([ + shipments = cls.search([ ('state', '=', 'draft'), ['OR', ('planned_date', '<=', today), ('planned_date', '=', None), ], ]) - for shipment in self.browse(shipment_ids): + for shipment in shipments: for move in shipment.moves: key = (shipment.to_location.id, shipment.from_location.id, move.product.id) if key not in moves: continue - quantity = uom_obj.compute_qty(move.uom, move.quantity, + quantity = Uom.compute_qty(move.uom, move.quantity, id2product[move.product.id].default_uom) moves[key] = max(0, moves[key] - quantity) @@ -85,27 +90,22 @@ class ShipmentInternal(ModelSQL, ModelView): shipments.setdefault( (from_location, to_location), []).append((product, qty)) # Create shipments and moves - for shipment, moves in shipments.iteritems(): - from_location, to_location = shipment - values = { - 'from_location': from_location, - 'to_location': to_location, - 'planned_date': today, - 'moves': [], - } + for locations, moves in shipments.iteritems(): + from_location, to_location = locations + shipment = cls( + from_location=from_location, + to_location=to_location, + planned_date=today, + moves=[], + ) for move in moves: product, qty = move - values['moves'].append( - ('create', - { - 'from_location': from_location, - 'to_location': to_location, - 'product': product, - 'quantity': qty, - 'uom': id2product[product].default_uom.id, - 'company': user_record.company.id, - } + shipment.moves.append(Move( + from_location=from_location, + to_location=to_location, + product=product, + quantity=qty, + uom=id2product[product].default_uom, + company=user_record.company, )) - self.create(values) - -ShipmentInternal() + shipment.save() diff --git a/tests/test_stock_supply.py b/tests/test_stock_supply.py index eef317f3c9..0dde8fbc8c 100644 --- a/tests/test_stock_supply.py +++ b/tests/test_stock_supply.py @@ -64,10 +64,7 @@ class StockSupplyTestCase(unittest.TestCase): ''' for purchase_date, delivery_time, supply_date in DATES: with Transaction().start(DB_NAME, USER, context=CONTEXT): - product_supplier_id = self.create_product_supplier( - delivery_time) - product_supplier = self.product_supplier.browse( - product_supplier_id) + product_supplier = self.create_product_supplier(delivery_time) date = self.product_supplier.compute_supply_date( product_supplier, purchase_date) self.assertEqual(date, supply_date) @@ -78,10 +75,7 @@ class StockSupplyTestCase(unittest.TestCase): ''' for purchase_date, delivery_time, supply_date in DATES: with Transaction().start(DB_NAME, USER, context=CONTEXT): - product_supplier_id = self.create_product_supplier( - delivery_time) - product_supplier = self.product_supplier.browse( - product_supplier_id) + product_supplier = self.create_product_supplier(delivery_time) date = self.product_supplier.compute_purchase_date( product_supplier, supply_date) self.assertEqual(date, purchase_date) @@ -93,48 +87,48 @@ class StockSupplyTestCase(unittest.TestCase): :param delivery_time: time in days needed to supply :return: the id of the Product Supplier ''' - uom_category_id = self.uom_category.create({'name': 'Test'}) - uom_id = self.uom.create({ + uom_category = self.uom_category.create({'name': 'Test'}) + uom = self.uom.create({ 'name': 'Test', 'symbol': 'T', - 'category': uom_category_id, + 'category': uom_category.id, 'rate': 1.0, 'factor': 1.0, }) - category_id = self.category.create({'name': 'ProdCategoryTest'}) - product_id = self.product.create({ + category = self.category.create({'name': 'ProdCategoryTest'}) + product = self.product.create({ 'name': 'ProductTest', - 'default_uom': uom_id, - 'category': category_id, + 'default_uom': uom.id, + 'category': category.id, 'account_category': True, 'list_price': Decimal(0), 'cost_price': Decimal(0), }) - company_id, = self.company.search([('name', '=', 'B2CK')]) - self.user.write(USER, { - 'main_company': company_id, - 'company': company_id, + company, = self.company.search([('name', '=', 'B2CK')]) + self.user.write([self.user(USER)], { + 'main_company': company.id, + 'company': company.id, }) - receivable_id, = self.account.search([ + receivable, = self.account.search([ ('kind', '=', 'receivable'), - ('company', '=', company_id), + ('company', '=', company.id), ]) - payable_id, = self.account.search([ + payable, = self.account.search([ ('kind', '=', 'payable'), - ('company', '=', company_id), + ('company', '=', company.id), ]) - supplier_id = self.party.create({ + supplier = self.party.create({ 'name': 'supplier', - 'account_receivable': receivable_id, - 'account_payable': payable_id, + 'account_receivable': receivable.id, + 'account_payable': payable.id, }) - product_supplier_id = self.product_supplier.create({ - 'product': product_id, - 'company': company_id, - 'party': supplier_id, + product_supplier = self.product_supplier.create({ + 'product': product.id, + 'company': company.id, + 'party': supplier.id, 'delivery_time': delivery_time, }) - return product_supplier_id + return product_supplier def suite():