from odoo import fields, models, api, tools, _ from datetime import datetime, timedelta, date from odoo.exceptions import UserError import logging import requests import json import re import qrcode, base64 from bs4 import BeautifulSoup from io import BytesIO _logger = logging.getLogger(__name__) class ProductTemplate(models.Model): _inherit = "product.template" x_studio_field_tGhJR = fields.Many2many('x_product_tags', string="Product Tags") x_manufacture = fields.Many2one( comodel_name="x_manufactures", string="Manufactures" ) x_model_product = fields.Char(string="Model Produk") x_product_manufacture = fields.Many2one( comodel_name="x_manufactures", string="Manufacture" ) x_lazada = fields.Text(string="Lazada") x_tokopedia = fields.Text(string="Tokopedia") web_tax_id = fields.Many2one('account.tax', string='Website Tax') web_price = fields.Float( 'Web Price', compute='_compute_web_price', digits='Product Price', inverse='_set_product_lst_price', help="Web Price with pricelist_id = 1") qty_stock_vendor = fields.Float('QTY Stock Vendor', compute='_compute_qty_stock_vendor') have_promotion_program = fields.Boolean('Have Promotion Program', compute='_have_promotion_program', help="Punya promotion program gak?") product_rating = fields.Float('Product Rating', help="Digunakan untuk sorting product di website", default=0.0) virtual_rating = fields.Float('Virtual Rating', compute='_compute_virtual_rating', help="Column Virtual untuk product rating, digunakan oleh Solr", default=0.0) last_calculate_rating = fields.Datetime("Last Calculate Rating") web_price_sorting = fields.Float('Web Price Sorting', help='Hanya digunakan untuk sorting di web, harga tidak berlaku', default=0.0) virtual_qty = fields.Float(string='Virtual Qty', default=0) solr_flag = fields.Integer(string='Solr Flag', default=0) search_rank = fields.Integer(string='Search Rank', default=0) search_rank_weekly = fields.Integer(string='Search Rank Weekly', default=0) supplier_url = fields.Char(string='Vendor URL') # custom field for support Trusco products maker_code = fields.Char(string='Maker Code') maker_name = fields.Char(string='Maker Name') origin = fields.Char(string='Origin') features = fields.Char(string='Features') usage = fields.Char(string='Usage') specification = fields.Char(string='Specification') material = fields.Char(string='Material') is_new_product = fields.Boolean(string='Produk Baru', help='Centang jika ingin ditammpilkan di website sebagai segment Produk Baru') seq_new_product = fields.Integer(string='Seq New Product', help='Urutan Sequence New Product') is_edited = fields.Boolean(string='Is Edited') qty_sold = fields.Float(string='Sold Quantity', compute='_get_qty_sold') kind_of = fields.Selection([ ('sp', 'Spare Part'), ('acc', 'Accessories') ], string='Kind of', copy=False) sni = fields.Boolean(string='SNI') tkdn = fields.Boolean(string='TKDN') short_spesification = fields.Char(string='Short Spesification') merchandise_ok = fields.Boolean(string='Product Promotion') print_barcode = fields.Boolean(string='Print Barcode', default=True) # qr_code = fields.Binary("QR Code", compute='_compute_qr_code') @api.model def create(self, vals): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) active_model = self.env.context.get('active_model') if self.env.user.id not in users_in_group.mapped('id') and active_model == None: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductTemplate, self).create(vals) return result # def write(self, values): # group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id # users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) # active_model = self.env.context.get('active_model') # if self.env.user.id not in users_in_group.mapped('id') and active_model == None: # raise UserError('Hanya MD yang bisa mengedit Product') # result = super(ProductTemplate, self).write(values) # return result # def _compute_qr_code(self): # for rec in self.product_variant_ids: # qr = qrcode.QRCode( # version=1, # error_correction=qrcode.constants.ERROR_CORRECT_L, # box_size=5, # border=4, # ) # qr.add_data(rec.display_name) # qr.make(fit=True) # img = qr.make_image(fill_color="black", back_color="white") # buffer = BytesIO() # img.save(buffer, format="PNG") # qr_code_img = base64.b64encode(buffer.getvalue()).decode() # rec.qr_code = qr_code_img @api.constrains('name', 'internal_reference', 'x_manufacture') def required_public_categ_ids(self): for rec in self: if not rec.public_categ_ids and rec.type == 'product': raise UserError('Field Categories harus diisi') def _get_qty_sold(self): for rec in self: rec.qty_sold = sum(x.qty_sold for x in rec.product_variant_ids) def day_product_to_edit(self): day_products = [] for product in self: day_product = (product.write_date - product.create_date).days day_products.append(day_product) return day_products @api.constrains('name', 'default_code') def _check_duplicate_product(self): for product in self: variants = product.product_variant_ids names = [x.name for x in variants] if variants else [product.name] default_codes = [x.default_code for x in variants] if variants else [product.default_code] domain = [ ('default_code', '!=', False), ('id', '!=', product.id), '|', ('name', 'in', names), ('default_code', 'in', default_codes) ] product_exist = self.search(domain, limit=1) if len(product_exist) > 0: raise UserError('Name atau Internal Reference sudah digunakan pada produk lain') if self.env.user.is_purchasing_manager or self.env.user.is_editor_product or self.env.user.id in [1, 25]: continue if sum(product.day_product_to_edit()) > 0: raise UserError('Produk ini tidak dapat diubah') @api.constrains('name') def _validate_name(self): rule_regex = self.env['ir.config_parameter'].sudo().get_param('product.product.rule_name_regex') or '' pattern = rf'^{rule_regex}$' if not re.match(pattern, self.name): pattern_suggest = rf"{rule_regex}" suggest = ''.join(re.findall(pattern_suggest, self.name)) raise UserError(f'Contoh yang benar adalah {suggest}') # def write(self, vals): # if 'solr_flag' not in vals and self.solr_flag == 1: # vals['solr_flag'] = 2 # return super().write(vals) def _compute_virtual_rating(self): for product in self: rate = 0 if product.web_price: rate += 8 if product.qty_sold > 0: rate += 5 if product.image_128: rate += 7 if product.website_description: rate += 7 if product.product_variant_id.qty_onhand_bandengan > 0: rate += 5 if product.product_variant_id._is_have_flashsale(): rate += 10 product.virtual_rating = rate # if product.web_price: # rate += 4 # if product.qty_sold > 0: # rate += 3 # if product.have_promotion_program: #have discount from pricelist # rate += 5 # if product.image_128: # rate += 3 # if product.website_description: # rate += 1 # if product.product_variant_id.qty_stock_vendor > 0: # rate += 2 # product.virtual_rating = rate def unlink(self): if self._name == 'product.template': raise UserError('Maaf anda tidak bisa delete product') def update_new_product(self): current_time = datetime.now() delta_time = current_time - timedelta(days=30) delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') products = self.env['product.template'].search([ ('type', '=', 'product'), ('active', '=', True), ('product_rating', '>', 3), ('create_date', '>=', delta_time), ], limit=100) seq = 0 for product in products: seq += 1 product.is_new_product = True product.seq_new_product = seq _logger.info('Updated New Product %s' % product.name) def update_internal_reference(self, limit=100): templates = self.env['product.template'].search([ ('default_code', '=', False), ('product_variant_ids.default_code', '=', False), ('type', '=', 'product'), ('active', '=', True) ], limit=limit, order='write_date desc') for template in templates: if not template.default_code: template.default_code = 'IT.'+str(template.id) for variant in template.product_variant_ids: if not variant.default_code: variant.default_code = 'ITV.%s' % str(variant.id) _logger.info('Updated Template %s' % template.name) # templates_with_variant = self.env['product.product'].search([ # ('default_code', '=', False), # ('type', '=', 'product'), # ('active', '=', True), # ('product_tmpl_id', '!=', False), # ], limit=limit, order='write_date desc') # for template_with_variant in templates_with_variant: # for product in template_with_variant.product_variant_ids: # if product.default_code: # continue # product.default_code = 'ITV.'+str(product.id) # _logger.info('Updated Variant %s' % product.name) @api.onchange('name','default_code','x_manufacture','product_rating','website_description','image_1920','weight','public_categ_ids') def update_solr_flag(self): for tmpl in self: if tmpl.solr_flag == 1: tmpl.solr_flag = 2 def _compute_qty_stock_vendor(self): for product_template in self: product_template.qty_stock_vendor = 0 for product_variant in product_template.product_variant_ids: product_template.qty_stock_vendor += int(product_variant.qty_stock_vendor) def _compute_web_price(self): for template in self: template.web_price = template.product_variant_id.web_price def _have_promotion_program(self): for template in self: # product = self.env['product.product'].search([('product_tmpl_id', '=', template.id)], limit=1) product_pricelist_item = self.env['product.pricelist.item'].search([ ('pricelist_id', '=', 4), ('product_id', '=', template.product_variant_id.id)], limit=1) discount = product_pricelist_item.price_discount if discount: template.have_promotion_program = True else: template.have_promotion_program = False def _get_active_flash_sale(self): current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') variant_ids = [x.id for x in self.product_variant_ids] pricelist = self.env['product.pricelist'].search([ ('is_flash_sale', '=', True), ('item_ids.product_id', 'in', variant_ids), ('start_date', '<=', current_time), ('end_date', '>=', current_time) ], limit=1) return pricelist @api.model def _calculate_rating_product(self, limit=1000, expiry_days=30, ids=False): current_time = datetime.now() current_time_str = current_time.strftime('%Y-%m-%d %H:%M:%S') delta_time = current_time - timedelta(days=expiry_days) delta_time_str = delta_time.strftime('%Y-%m-%d %H:%M:%S') query = [ '&','&', ('type', '=', 'product'), ('active', '=', True) ] if not ids: query += [ '|', ('last_calculate_rating', '=', False), ('last_calculate_rating', '<', delta_time_str) ] else: query += [('id', 'in', ids)] products = self.env['product.template'].search(query, limit=limit) for product in products: _logger.info("Calculate Rating Product %s" % product.id) product.product_rating = product.virtual_rating product.last_calculate_rating = current_time_str def _get_stock_website(self): qty = self._get_stock_altama() print(qty) def get_stock_altama(self, item_code): current_time = datetime.now() current_time = current_time.strftime('%Y-%m-%d %H:%M:%S') query = [('source', '=', 'altama'), ('expired_date', '>', current_time)] token_data = self.env['token.storage'].search(query, order='expired_date desc',limit=1) if not token_data: token_data = self._get_new_token_altama() token = token_data['access_token'] else: token = token_data.access_token url = "https://erpapi.altama.co.id/erp/api/stock/buffer/btob" auth = "Bearer "+token headers = { 'Content-Type': 'application/json', 'Authorization': auth, } json_data = { 'type_search': 'Item_code', 'search_key':[item_code], } response = requests.post(url, headers=headers, json=json_data) datas = json.loads(response.text)['data'] qty = 0 for data in datas: availability = float(data['availability']) # Mengonversi ke tipe data int qty += availability # Mengakumulasi qty dari setiap data return qty def _get_new_token_altama(self): url = "https://kc.altama.co.id/realms/altama/protocol/openid-connect/token" auth = 'Basic SW5kb3Rla25pa19DbGllbnQ6Vm1iZExER1ZUS3RuVlRQdkU1MXRvRzdiTW51TE1WRVI=' headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': auth, } data = { 'grant_type': 'client_credentials', } response = requests.post(url, headers=headers, data=data).json() lookup_json = json.dumps(response, indent=4, sort_keys=True) token = json.loads(lookup_json)['access_token'] expires_in = json.loads(lookup_json)['expires_in'] current_time = datetime.now() delta_time = current_time + timedelta(seconds=int(expires_in)) current_time = current_time.strftime('%Y-%m-%d %H:%M:%S') delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') values = { 'source': 'altama', 'access_token': token, 'expires_in': expires_in, 'expired_date': delta_time, } self.env['token.storage'].create([values]) return values def write(self, vals): # for rec in self: # if rec.id == 224484: # raise UserError('Tidak dapat mengubah produk sementara') return super(ProductTemplate, self).write(vals) class ProductProduct(models.Model): _inherit = "product.product" web_price = fields.Float( 'Web Price', compute='_compute_web_price', digits='Product Price', inverse='_set_product_lst_price', help="Web Price with pricelist_id = 1") qty_stock_vendor = fields.Float( 'Qty Stock Vendor', compute='_compute_stock_vendor', help="Stock Vendor") solr_flag = fields.Integer(string='Solr Flag', default=0) # custom field for support Trusco products maker_code = fields.Char(string='Maker Code') maker_name = fields.Char(string='Maker Name') origin = fields.Char(string='Origin') features = fields.Char(string='Features') usage = fields.Char(string='Usage') specification = fields.Char(string='Specification') material = fields.Char(string='Material') qty_onhand_bandengan = fields.Float(string='Onhand BU', compute='_get_qty_onhand_bandengan') clean_website_description = fields.Char(string='Clean Website Description', compute='_get_clean_website_description') qty_incoming_bandengan = fields.Float(string='Incoming BU', compute='_get_qty_incoming_bandengan') qty_outgoing_bandengan = fields.Float(string='Outgoing BU', compute='_get_qty_outgoing_bandengan') qty_available_bandengan = fields.Float(string='Available BU', compute='_get_qty_available_bandengan') qty_free_bandengan = fields.Float(string='Free BU', compute='_get_qty_free_bandengan') qty_upcoming = fields.Float(string='Qty Upcoming', compute='_get_qty_upcoming') sla_version = fields.Integer(string="SLA Version", default=0) is_edited = fields.Boolean(string='Is Edited') qty_sold = fields.Float(string='Sold Quantity', compute='_get_qty_sold') short_spesification = fields.Char(string='Short Spesification') max_qty_reorder = fields.Float(string='Max Qty Reorder', compute='_get_max_qty_reordering_rule') qty_rpo = fields.Float(string='Qty RPO', compute='_get_qty_rpo') plafon_qty = fields.Float(string='Max Plafon', compute='_get_plafon_qty_product') merchandise_ok = fields.Boolean(string='Product Promotion') qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant') qty_pcs_box = fields.Float("Pcs Box") barcode_box = fields.Char("Barcode Box") def generate_product_sla(self): product_variant_ids = self.env.context.get('active_ids', []) product_variant = self.search([('id', 'in', product_variant_ids)]) sla_record = self.env['product.sla'].search([('product_variant_id', '=', product_variant.id)], limit=1) if sla_record: sla_record.generate_product_sla() else: new_sla_record = self.env['product.sla'].create({ 'product_variant_id': product_variant.id, }) new_sla_record.generate_product_sla() @api.model def create(self, vals): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id active_model = self.env.context.get('active_model') users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) if self.env.user.id not in users_in_group.mapped('id') and active_model == None: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductProduct, self).create(vals) return result # def write(self, values): # group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id # active_model = self.env.context.get('active_model') # users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) # if self.env.user.id not in users_in_group.mapped('id') and active_model == None: # raise UserError('Hanya MD yang bisa mengedit Product') # result = super(ProductProduct, self).write(values) # return result def _compute_qr_code_variant(self): for rec in self: # Skip inactive variants if not rec.active: rec.qr_code_variant = False # Clear the QR Code for archived variants continue qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=4, ) qr.add_data(rec.barcode if rec.barcode else rec.default_code) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffer = BytesIO() img.save(buffer, format="PNG") qr_code_img = base64.b64encode(buffer.getvalue()).decode() rec.qr_code_variant = qr_code_img def _get_clean_website_description(self): for rec in self: cleaned_desc = BeautifulSoup(self.website_description or '', "html.parser").get_text() rec.clean_website_description = cleaned_desc @api.constrains('name', 'internal_reference', 'x_manufacture') def required_public_categ_ids(self): for rec in self: if not rec.public_categ_ids and rec.type == 'product': raise UserError('Field Categories harus diisi') @api.constrains('active') def archive_product(self): for product in self: if self.env.context.get('skip_unpublished_constraint'): continue # Mencegah looping saat dipanggil dari metode lain product_template = product.product_tmpl_id variants = product_template.product_variant_ids if len(variants) == 1: # Jika hanya ada satu varian, atur status `unpublished` berdasarkan `active` product_template.with_context(skip_unpublished_constraint=True).unpublished = not product.active product.with_context(skip_unpublished_constraint=True).unpublished = not product.active else: if product.active: product.with_context(skip_unpublished_constraint=True).unpublished = False product_template.with_context(skip_unpublished_constraint=True).unpublished = any(variant.active for variant in variants) else: product.with_context(skip_unpublished_constraint=True).unpublished = True all_inactive = all(not variant.active for variant in variants) product_template.with_context(skip_unpublished_constraint=True).unpublished = all_inactive @api.constrains('unpublished') def archive_product_unpublished(self): for product in self: if self.env.context.get('skip_active_constraint'): continue # Mencegah looping saat dipanggil dari metode lain product_template = product.product_tmpl_id variants = product_template.product_variant_ids if len(variants) == 1: # Jika hanya ada satu varian, atur status `unpublished` pada template, tetapi biarkan `active` tetap True product_template.with_context(skip_active_constraint=True).unpublished = product.unpublished else: if not product.unpublished: # Jika `unpublished` adalah False, pastikan `active` tetap True product.with_context(skip_active_constraint=True).active = True product_template.with_context(skip_active_constraint=True).active = any(not variant.unpublished for variant in variants) else: # Jika `unpublished` adalah True, atur template hanya jika semua varian di-unpublished all_unpublished = all(variant.unpublished for variant in variants) product_template.with_context(skip_active_constraint=True).active = not all_unpublished def update_internal_reference_variants(self, limit=100): variants = self.env['product.product'].search([ ('default_code', '=', False), ('type', '=', 'product'), ('active', '=', True) ], limit=limit, order='write_date desc') for variant in variants: if not variant.default_code: variant.default_code = 'ITV.'+str(variant.id) _logger.info('Updated variant %s' % variant.name) def _get_po_suggest(self, qty_purchase): if self.qty_available_bandengan < qty_purchase: return 'harus beli' return 'masih cukup' def _get_qty_upcoming(self): for product in self: product.qty_upcoming = product.incoming_qty + product.qty_available def _get_qty_sold(self): for product in self: order_line = self.env['sale.order.line'].search([ ('order_id.state', 'in', ['done', 'sale']), ('product_id', '=', product.id) ]) product.qty_sold = sum(x.product_uom_qty for x in order_line) def day_product_to_edit(self): day_products = [] for product in self: day_product = (product.write_date - product.create_date).days day_products.append(day_product) return day_products @api.constrains('name') def _validate_name(self): rule_regex = self.env['ir.config_parameter'].sudo().get_param('product.product.rule_name_regex') or '' pattern = rf'^{rule_regex}$' if not re.match(pattern, self.name): pattern_suggest = rf"{rule_regex}" suggest = ''.join(re.findall(pattern_suggest, self.name)) raise UserError(f'Contoh yang benar adalah {suggest}') def _get_qty_incoming_bandengan(self): for product in self: qty = self.env['v.move.outstanding'].read_group( domain=[ ('product_id', '=', product.id), ('location_dest_id', 'in', [57, 83]), ], fields=['qty_need'], groupby=[] )[0].get('qty_need', 0.0) product.qty_incoming_bandengan = qty def _get_qty_incoming_bandengan_with_exclude(self): for product in self: qty = self.env['v.move.outstanding'].read_group( domain=[ ('product_id', '=', product.id), ('location_dest_id', 'in', [57, 83]), ], fields=['qty_need'], groupby=[] )[0].get('qty_need', 0.0) product.qty_incoming_bandengan = qty def _get_qty_outgoing_bandengan(self): for product in self: qty = self.env['v.move.outstanding'].read_group( domain=[ ('product_id', '=', product.id), ('location_id', 'in', [57, 83]), ], fields=['qty_need'], groupby=[] )[0].get('qty_need', 0.0) product.qty_outgoing_bandengan = qty def _get_qty_onhand_bandengan(self): for product in self: qty_onhand = self.env['stock.quant'].search([ ('product_id', '=', product.id), ('location_id', 'in', [57, 83]) ]) qty = sum(qty_onhand.mapped('quantity')) product.qty_onhand_bandengan = qty def _get_qty_available_bandengan(self): for product in self: qty_available = product.qty_incoming_bandengan + product.qty_onhand_bandengan - product.qty_outgoing_bandengan product.qty_available_bandengan = qty_available or 0 def _get_qty_free_bandengan(self): for product in self: qty_free = product.qty_onhand_bandengan - product.qty_outgoing_bandengan product.qty_free_bandengan = qty_free def _get_max_qty_reordering_rule(self): for product in self: reordering = self.env['stock.warehouse.orderpoint'].search([ ('product_id', '=', product.id) ], limit=1) if not reordering: product.max_qty_reorder = 0 else: product.max_qty_reorder = reordering.product_max_qty def _get_qty_rpo(self): for product in self: rpo = self.env['v.requisition.match.po'].search([ ('product_id', '=', product.id) ], limit=1) if not rpo: product.qty_rpo = 0 else: product.qty_rpo = rpo.qty_rpo def _get_plafon_qty_product(self): for product in self: qty_available = product.qty_available_bandengan max_qty = product.max_qty_reorder qty_rpo = product.qty_rpo product.plafon_qty = max_qty - qty_available + qty_rpo # def write(self, vals): # if 'solr_flag' not in vals: # for variant in self: # if variant.solr_flag == 1: # variant.product_tmpl_id.solr_flag = 2 # vals['solr_flag'] = 2 # return super().write(vals) def _compute_web_price(self): for product in self: pricelist_id = self.env['ir.config_parameter'].sudo().get_param('product.pricelist.default_price_id_v2') domain = [('pricelist_id.id', '=', pricelist_id or 17022), ('product_id.id', '=', product.id)] product_pricelist_item = self.env['product.pricelist.item'].search(domain, limit=1) if product_pricelist_item.base_pricelist_id: base_pricelist_id = product_pricelist_item.base_pricelist_id.id domain = [('pricelist_id', '=', base_pricelist_id), ('product_id', '=', product.id)] product_pricelist_item = self.env['product.pricelist.item'].search(domain, limit=1) product.web_price = product_pricelist_item.fixed_price def _compute_stock_vendor(self): for product in self: stock_vendor = self.env['stock.vendor'].search([('product_variant_id', '=', product.id)], limit=1) product.qty_stock_vendor = stock_vendor.quantity + product.qty_available def unlink(self): if self._name == 'product.product': raise UserError('Maaf anda tidak bisa delete product') def _get_active_flash_sale(self): current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') pricelist = self.env['product.pricelist'].search([ ('is_flash_sale', '=', True), ('item_ids.product_id', '=', self.id), ('start_date', '<=', current_time), ('end_date', '>=', current_time) ], limit=1) return pricelist class OutstandingMove(models.Model): _name = 'v.move.outstanding' _auto = False _rec_name = 'id' id = fields.Integer(string='ID') product_id = fields.Many2one('product.product', string='Product') reference = fields.Char(string='Reference', help='Nomor Dokumen terkait') qty_need = fields.Float(string='Qty Need', help='Qty yang akan outgoing / incoming') location_id = fields.Many2one('stock.location', string='Location', help='Lokasi asal') location_dest_id = fields.Many2one('stock.location', string='Location To', help='Lokasi tujuan') def init(self): # where clause 'state in' follow the origin of outgoing and incoming odoo tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(""" CREATE OR REPLACE VIEW %s AS select sm.id, sm.reference, sm.product_id, sm.product_uom_qty as qty_need, sm.location_id, sm.location_dest_id from stock_move sm where 1=1 and sm.state in( 'waiting', 'confirmed', 'assigned', 'partially_available' ) """ % self._table)