class PurchaseOrderLine(models.Model): _inherit = "purchase.order.line" @api.depends('discount2', 'discount3') def _compute_amount(self): super(PurchaseOrderLine, self)._compute_amount() discount2 = fields.Float( 'Disc. 2 (%)', digits=dp.get_precision('Discount'), ) discount3 = fields.Float( 'Disc. 3 (%)', digits=dp.get_precision('Discount'), ) _sql_constraints = [ ('discount2_limit', 'CHECK (discount2 <= 100.0)', 'Discount 2 must be lower than 100%.'), ('discount3_limit', 'CHECK (discount3 <= 100.0)', 'Discount 3 must be lower than 100%.'), ] def _get_discounted_price_unit(self): price_unit = super(PurchaseOrderLine, self)._get_discounted_price_unit() if self.discount2: price_unit *= (1 - self.discount2 / 100.0) if self.discount3: price_unit *= (1 - self.discount3 / 100.0) return price_unit @api.onchange('product_qty', 'product_uom') def _onchange_quantity(self): """ Check if a discount is defined into the supplier info and if so then apply it to the current purchase order line """ res = super(PurchaseOrderLine, self)._onchange_quantity() if self.product_id: date = None if self.order_id.date_order: date = fields.Date.to_string( fields.Date.from_string(self.order_id.date_order)) product_supplierinfo = self.product_id._select_seller( partner_id=self.partner_id, quantity=self.product_qty, date=date, uom_id=self.product_uom) if product_supplierinfo: self.discount2 = product_supplierinfo.discount2 self.discount3 = product_supplierinfo.discount3 return res
class ResPartner(models.Model): _inherit = "res.partner" default_supplierinfo_discount2 = fields.Float( string='Default Supplier Discount 2 (%)', digits=dp.get_precision('Discount'), help="This value will be used as the default one, for each new " "supplierinfo line depending on that supplier.", ) default_supplierinfo_discount3 = fields.Float( string='Default Supplier Discount 3 (%)', digits=dp.get_precision('Discount'), help="This value will be used as the default one, for each new " "supplierinfo line depending on that supplier.", )
class PurchaseOrderRecommendationLine(models.TransientModel): _inherit = 'purchase.order.recommendation.line' secondary_uom_id = fields.Many2one( comodel_name='product.secondary.unit', related='product_id.purchase_secondary_uom_id', readonly=True, ) secondary_uom_qty = fields.Float( string='Secondary Qty', digits=dp.get_precision('Product Unit of Measure'), ) @api.onchange('secondary_uom_id', 'secondary_uom_qty') def _onchange_secondary_uom(self): if not self.secondary_uom_id: return factor = self.secondary_uom_id.factor * self.product_id.uom_id.factor qty = float_round( self.secondary_uom_qty * factor, precision_rounding=self.product_id.uom_id.rounding) if float_compare( self.units_included, qty, precision_rounding=self.product_id.uom_id.rounding) != 0: self.units_included = qty @api.onchange('units_included') def _onchange_units_included_purchase_order_secondary_unit(self): if not self.secondary_uom_id: return factor = self.secondary_uom_id.factor * self.product_id.uom_id.factor qty = float_round( self.units_included / (factor or 1.0), precision_rounding=self.secondary_uom_id.uom_id.rounding) if float_compare( self.secondary_uom_qty, qty, precision_rounding=self.secondary_uom_id.uom_id.rounding) != 0: self.secondary_uom_qty = qty @api.multi def _prepare_update_po_line(self): res = super()._prepare_update_po_line() if self.secondary_uom_id and self.secondary_uom_qty: res.update({ 'secondary_uom_id': self.secondary_uom_id.id, 'secondary_uom_qty': self.secondary_uom_qty, }) return res @api.multi def _prepare_new_po_line(self, sequence): res = super()._prepare_new_po_line(sequence) if self.secondary_uom_id and self.secondary_uom_qty: res.update({ 'secondary_uom_id': self.secondary_uom_id.id, 'secondary_uom_qty': self.secondary_uom_qty, }) return res
class product_attribute_value(models.Model): _inherit = "product.attribute.value" lst_price_brut = fields.Float( string='Gross selling price', digits_compute=dp.get_precision('Product Price'), ) @api.multi def write(self, vals): if self.env.context.get('active_id', False): price_att = self.env['product.attribute.price'].search([ ('product_tmpl_id', '=', self.env.context['active_id']), ('value_id', '=', self.id) ]) if price_att: template = price_att.product_tmpl_id if 'price_extra' in vals: vals['lst_price_brut'] = template.lst_price_brut + vals[ 'price_extra'] * template.brut_net_factor elif 'lst_price_brut' in vals: vals['price_extra'] = ( vals['lst_price_brut'] - template.lst_price_brut) / template.brut_net_factor elif 'lst_price_brut' in vals: del vals['lst_price_brut'] return super(product_attribute_value, self).write(vals)
class AccountInvoiceLine(models.Model): _inherit = "account.invoice.line" discount2 = fields.Float( 'Discount 2 (%)', digits=dp.get_precision('Discount'), default=0.0, ) discount3 = fields.Float( 'Discount 3 (%)', digits=dp.get_precision('Discount'), default=0.0, ) def _get_discounted_price_unit(self): """Inheritable method for getting the unit price after applying discount(s). :returns: Unit price after discount(s). :rtype: float """ self.ensure_one() price_unit = self.price_unit if self.discount: price_unit *= (1 - self.discount / 100) if self.discount2: price_unit *= (1 - self.discount2 / 100.0) if self.discount3: price_unit *= (1 - self.discount3 / 100.0) return price_unit @api.multi @api.depends('discount2', 'discount3') def _compute_price(self): for line in self: prev_price_unit = line.price_unit prev_discount = line.discount price_unit = line._get_discounted_price_unit() line.update({ 'price_unit': price_unit, 'discount': 0.0, }) super(AccountInvoiceLine, line)._compute_price() line.update({ 'price_unit': prev_price_unit, 'discount': prev_discount, })
class ResPartner(models.Model): _inherit = 'res.partner' purchase_general_discount = fields.Float( digits=dp.get_precision('Discount'), string='Purchase General Discount (%)', company_dependent=True, )
class mass_mo_line(osv.osv_memory): _name = "mo.batch.line" _description = "Manufacturing Orders Line" _columns = { 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('internal_type','=','Finish'),('bom_ids','!=',False),('bom_ids.bom_id','=',False)]), 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True), 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True), 'product_uos_qty': fields.float('Product UoS Quantity'), 'product_uos': fields.many2one('product.uom', 'Product UoS'), 'bom_id': fields.many2one('mrp.bom', 'Blend (BoM)', required=True, domain=[('bom_id','=',False)], help="Bill of Materials allow you to define the list of required raw materials to make a finished product."), 'date_planned': fields.datetime('Scheduled Date', required=True, select=1), 'company_id': fields.many2one('res.company', 'Company', required=True), 'mo_batch_id': fields.many2one('mo.batch', 'MO'), } _defaults = { 'date_planned': lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), 'product_qty': lambda *a: 1.0, 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c), } def product_id_change(self, cr, uid, ids, product_id, context=None): """ Finds UoM of changed product. @param product_id: Id of changed product. @return: Dictionary of values. """ if not product_id: return {'value': { 'product_uom': False, 'bom_id': False, }} bom_obj = self.pool.get('mrp.bom') product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) product_uom_id = product.uom_id and product.uom_id.id or False bom_id = bom_obj._bom_find(cr, uid, product.id, product_uom_id, []) result = { 'product_uom': product_uom_id, 'bom_id': bom_id, } return {'value': result} def _prepare_mo_batch_line(self, cr, uid, line, context=None): vals = { 'origin': 'Create from batch', 'product_id': line.product_id.id, 'product_qty': line.product_qty, 'product_uom': line.product_uom.id, 'product_uos_qty': line.product_uos and line.product_uos_qty or False, 'product_uos': line.product_uos and line.product_uos.id or False, 'bom_id': line.bom_id and line.bom_id.id or False, 'date_planned': line.date_planned, 'user_id': line.mo_batch_id.user_id.id, 'company_id': line.company_id.id, } return vals
class PurchaseOrderLine(models.Model): _inherit = 'purchase.order.line' secondary_uom_qty = fields.Float( string='Secondary Qty', digits=dp.get_precision('Product Unit of Measure'), ) secondary_uom_id = fields.Many2one( comodel_name='product.secondary.unit', string='Secondary uom', ondelete='restrict', ) @api.onchange('secondary_uom_id', 'secondary_uom_qty') def _onchange_secondary_uom(self): if not self.secondary_uom_id: return factor = self.secondary_uom_id.factor * self.product_uom.factor qty = float_round(self.secondary_uom_qty * factor, precision_rounding=self.product_uom.rounding) if float_compare(self.product_qty, qty, precision_rounding=self.product_uom.rounding) != 0: self.product_qty = qty @api.onchange('product_qty') def _onchange_product_qty_purchase_order_secondary_unit(self): if not self.secondary_uom_id: return factor = self.secondary_uom_id.factor * self.product_uom.factor qty = float_round( self.product_qty / (factor or 1.0), precision_rounding=self.secondary_uom_id.uom_id.rounding) if float_compare( self.secondary_uom_qty, qty, precision_rounding=self.secondary_uom_id.uom_id.rounding) != 0: self.secondary_uom_qty = qty @api.onchange('product_uom') def _onchange_product_uom_purchase_order_secondary_unit(self): if not self.secondary_uom_id: return factor = self.product_uom.factor * self.secondary_uom_id.factor qty = float_round(self.product_qty / (factor or 1.0), precision_rounding=self.product_uom.rounding) if float_compare(self.secondary_uom_qty, qty, precision_rounding=self.product_uom.rounding) != 0: self.secondary_uom_qty = qty @api.onchange('product_id') def _onchange_product_id_purchase_order_secondary_unit(self): self.secondary_uom_id = self.product_id.purchase_secondary_uom_id
class StockSecondaryUnitMixin(models.AbstractModel): _name = 'stock.secondary.unit.mixin' secondary_uom_id = fields.Many2one( comodel_name='product.secondary.unit', string='Second unit', ) secondary_uom_qty = fields.Float( string='Secondary Qty', digits=dp.get_precision('Product Unit of Measure'), )
class GlobalDiscount(models.Model): _name = 'global.discount' _description = 'Global Discount' _order = "sequence, id desc" sequence = fields.Integer(help='Gives the order to apply discounts', ) name = fields.Char( string='Discount Name', required=True, ) discount = fields.Float( digits=dp.get_precision('Discount'), required=True, default=0.0, ) discount_scope = fields.Selection( selection=[ ('sale', 'Sales'), ('purchase', 'Purchases'), ], default='sale', required='True', string='Discount Scope', ) company_id = fields.Many2one( comodel_name='res.company', string='Company', default=lambda self: self.env.user.company_id, ) def name_get(self): result = [] for one in self: result.append( (one.id, '{} ({:.2f}%)'.format(one.name, one.discount))) return result def _get_global_discount_vals(self, base, **kwargs): """ Prepare the dict of values to create to obtain the discounted amount :param float base: the amount to discount :return: dict with the discounted amount """ self.ensure_one() return { 'global_discount': self, 'base': base, 'base_discounted': base * (1 - (self.discount / 100)), }
class StockProductSecondaryUnit(models.AbstractModel): _name = 'stock.product.secondary.unit' secondary_unit_qty_available = fields.Float( string='Quantity On Hand (2Unit)', compute='_compute_secondary_unit_qty_available', digits=dp.get_precision('Product Unit of Measure'), ) def _compute_secondary_unit_qty_available(self): for product in self.filtered('stock_secondary_uom_id'): qty = product.qty_available / ( product.stock_secondary_uom_id.factor or 1.0) product.secondary_unit_qty_available = float_round( qty, precision_rounding=product.uom_id.rounding)
class product_template(models.Model): _inherit = 'product.template' list_price = fields.Float(compute='_compute_net_price', ) lst_price_brut = fields.Float( string='Gross selling price', digits_compute=dp.get_precision('Product Price'), ) brut_net_factor = fields.Float(string='Gross/Net Ratio', default=1) def get_list_price_factor(self, product, request): cr, uid, context, registry = request.cr, request.uid, request.context, request.registry orm_user = registry.get('res.users') partner = orm_user.browse(cr, SUPERUSER_ID, request.uid, context).partner_id factor = 1 if hasattr(partner.property_account_position, 'b2c_fiscal_position' ) and partner.property_account_position.b2c_fiscal_position: if product.brut_net_factor > 0: factor = product.brut_net_factor return factor @api.onchange('categ_id') def onchange_product_category(self): if self.categ_id.brut_net_factor > 0: self.brut_net_factor = self.categ_id.brut_net_factor @api.one @api.depends('lst_price_brut', 'brut_net_factor') def _compute_net_price(self): if self.brut_net_factor: self.list_price = self.lst_price_brut / self.brut_net_factor if 'request' not in self.env.context and 'uid' in self.env.context: variants_attribute_prices = self.env[ 'product.attribute.price'].search([('product_tmpl_id', '=', self.id)]) for attribute_price in variants_attribute_prices: value = attribute_price.value_id _logger.debug("Variant: %s", value.name) if value: price_extra = value.with_context( active_id=self.id).price_extra value.with_context(source='template').sudo().write({ 'lst_price_brut': (self.list_price + price_extra) * self.brut_net_factor })
class ProductConfiguratorAttribute(models.Model): _name = 'product.configurator.attribute' owner_id = fields.Integer( string="Owner", required=True, # ondelete is required since the owner_id is declared as inverse # of the field product_attribute_ids of the abstract model # product.configurator ondelete='cascade') owner_model = fields.Char(required=True) product_tmpl_id = fields.Many2one(comodel_name='product.template', string='Product Template', required=True) attribute_id = fields.Many2one(comodel_name='product.attribute', string='Attribute', readonly=True) value_id = fields.Many2one(comodel_name='product.attribute.value', domain="[('attribute_id', '=', attribute_id), " " ('id', 'in', possible_value_ids)]", string='Value') possible_value_ids = fields.Many2many( comodel_name='product.attribute.value', compute='_compute_possible_value_ids', readonly=True) price_extra = fields.Float( compute='_compute_price_extra', digits=dp.get_precision('Product Price'), help="Price Extra: Extra price for the variant with this attribute " "value on sale price. eg. 200 price extra, 1000 + 200 = 1200.") @api.depends('attribute_id') def _compute_possible_value_ids(self): for record in self: # This should be unique due to the new constraint added attribute = record.product_tmpl_id.attribute_line_ids.filtered( lambda x: x.attribute_id == record.attribute_id) record.possible_value_ids = attribute.value_ids.sorted() @api.depends('value_id') def _compute_price_extra(self): for record in self: record.price_extra = sum( record.value_id.price_ids.filtered(lambda x: ( x.product_tmpl_id == record.product_tmpl_id)).mapped( 'price_extra'))
class ProductProduct(models.Model): _inherit = "product.product" extra_weight = fields.Float( 'Extra Weight', digits=dp.get_precision('Stock Weight'), help="Extra weight (in Kg.) given by packaging, etc.", ) @api.onchange('extra_weight') def _onchange_extra_weight(self): if self.is_weight_uom: self.weight = self.uom_id.factor_inv + self.extra_weight else: self.extra_weight = 0 @api.onchange('uom_id') def _onchange_uom_product_weight_through_uom(self): # When a UoM is in Weight category, we set the product weight to its # corresponding conversion to Kg. if self.is_weight_uom: self.weight = self.uom_id.factor_inv + self.extra_weight @api.multi def write(self, vals): if vals.get('uom_id') or vals.get('extra_weight'): uom_id = (self.env['product.uom'].browse(vals.get('uom_id')) or self.uom_id) if self.is_weight_uom: vals['weight'] = uom_id.factor_inv + ( vals.get('extra_weight') or self.extra_weight) return super().write(vals) @api.model def create(self, vals): template = self.env['product.template'].browse( vals.get('product_tmpl_id')) uom_id = template.is_weight_uom and template.uom_id if not template: uom_id = (self.env['product.uom'].browse(vals.get('uom_id'))) if uom_id and uom_id.category_id == self.env.ref( 'product.product_uom_categ_kgm'): vals['weight'] = uom_id.factor_inv + ( vals.get('extra_weight') or template.extra_weight) return super().create(vals)
class StockInventoryLine(models.Model): _inherit = 'stock.inventory.line' discrepancy_qty = fields.Float( string='Discrepancy', compute='_compute_discrepancy', help="The difference between the actual qty counted and the " "theoretical quantity on hand.", digits=dp.get_precision('Product Unit of Measure'), default=0) discrepancy_percent = fields.Float( string='Discrepancy percent (%)', compute='_compute_discrepancy', digits=(3, 2), help="The discrepancy expressed in percent with theoretical quantity " "as basis") discrepancy_threshold = fields.Float( string='Threshold (%)', digits=(3, 2), help="Maximum Discrepancy Rate Threshold", compute='_compute_discrepancy_threshold') @api.multi @api.depends('theoretical_qty', 'product_qty') def _compute_discrepancy(self): for line in self: line.discrepancy_qty = line.product_qty - line.theoretical_qty if line.theoretical_qty: line.discrepancy_percent = 100 * abs( (line.product_qty - line.theoretical_qty) / line.theoretical_qty) elif not line.theoretical_qty and line.product_qty: line.discrepancy_percent = 100.0 @api.multi def _compute_discrepancy_threshold(self): for line in self: whs = line.location_id.get_warehouse() if line.location_id.discrepancy_threshold > 0.0: line.discrepancy_threshold = line.location_id.\ discrepancy_threshold elif whs.discrepancy_threshold > 0.0: line.discrepancy_threshold = whs.discrepancy_threshold else: line.discrepancy_threshold = False
class StockInventoryEmptyLines(models.Model): _name = 'stock.inventory.line.empty' product_code = fields.Char( string='Product Code', required=True, ) product_qty = fields.Float( string='Quantity', required=True, default=1.0, digits=dp.get_precision('Product Unit of Measure'), ) inventory_id = fields.Many2one( comodel_name='stock.inventory', string='Inventory', required=True, ondelete="cascade", )
class PurchaseCostDistributionLineExpense(models.Model): _name = "purchase.cost.distribution.line.expense" _description = "Purchase cost distribution line expense" distribution_line = fields.Many2one( comodel_name='purchase.cost.distribution.line', string='Cost distribution line', ondelete="cascade", ) picking_id = fields.Many2one( comodel_name="stock.picking", store=True, readonly=True, related="distribution_line.picking_id", ) picking_date_done = fields.Datetime( related="picking_id.date_done", store=True, readonly=True, ) distribution_expense = fields.Many2one( comodel_name='purchase.cost.distribution.expense', string='Distribution expense', ondelete="cascade", ) type = fields.Many2one( 'purchase.expense.type', string='Expense type', readonly=True, related='distribution_expense.type', store=True, ) expense_amount = fields.Float( string='Expense amount', digits=dp.get_precision('Account'), ) cost_ratio = fields.Float('Unit cost') company_id = fields.Many2one( comodel_name="res.company", related="distribution_line.company_id", store=True, readonly=True, )
class StockPickingScrapLine(models.TransientModel): _name = "wiz.stock.picking.scrap.line" _rec_name = 'product_id' product_id = fields.Many2one( comodel_name='product.product', string='Product', readonly=True, ) lot_id = fields.Many2one( comodel_name='stock.production.lot', string='Lot', readonly=True, ) package_id = fields.Many2one( comodel_name='stock.quant.package', string='Package', readonly=True, ) owner_id = fields.Many2one( comodel_name='res.partner', string='Owner', readonly=True, ) quantity = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, ) uom_id = fields.Many2one( comodel_name='product.uom', string='Unit of Measure', readonly=True, ) wizard_id = fields.Many2one( comodel_name='wiz.stock.picking.scrap', string='Wizard', ) move_line_id = fields.Many2one( comodel_name='stock.move.line', string='Move Line', )
class CXOrderpointTemplate(models.Model): _name = "cx.orderpoint.template" _description = "Minimum Inventory Rule Template" name = fields.Char(string="Name", compute='name_compose', store=False) active = fields.Boolean(string='Active', default=True) category_id = fields.Many2one(string='Product Category', comodel_name='product.category', copy=False, ondelete='cascade') attr_val_ids = fields.Many2many(string="Attribute Values", comodel_name='product.attribute.value', relation='cx_op_tpl_pr_attr_val_rel', column1='cx_op_template_id', column2='attr_val_id') product_min_qty = fields.Float(string='Minimum Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) product_max_qty = fields.Float(string='Maximum Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) qty_multiple = fields.Float(string='Qty Multiple', digits=dp.get_precision('Product Unit of Measure'), default=1, required=True) group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) company_id = fields.Many2one(string='Company', comodel_name='res.company', required=True, default=lambda self: self.env.user.company_id.id) lead_days = fields.Integer(sting='Lead Time', default=1) lead_type = fields.Selection( [('net', 'Day(s) to get the products'), ('supplier', 'Day(s) to purchase')], 'Lead Type', required=True, default='supplier') warehouse_id = fields.Many2one(comodel_name='stock.warehouse', string='Warehouse', ondelete="cascade", required=False) location_id = fields.Many2one(comodel_name='stock.location', string='Location', ondelete="cascade", required=False) rule_ids = fields.One2many(string="Related Rules", comodel_name='stock.warehouse.orderpoint', inverse_name='template_id') rule_ids_count = fields.Integer(string="Rules Count", compute="_rule_ids_count") _sql_constraints = [('tmpl_name_uniq', 'unique(category_id, company_id)', 'Such template already exists for this company!')] # -- Count related rules @api.depends('rule_ids') @api.multi def _rule_ids_count(self): for rec in self: rec.rule_ids_count = len(rec.rule_ids) # -- Compose name @api.depends('category_id') @api.multi def name_compose(self): for rec in self: prefix = rec.category_id.name_get()[0][1] if rec.category_id else False if prefix: rec.name = prefix else: rec.name = _('Global') # -- Compose name -- PRO @api.depends('category_id', 'attr_val_ids') @api.multi def name_compose_pro(self): self.ensure_one() prefix = self.category_id.name if self.category_id else False suffix = [attr_val_id.name for attr_val_id in self.attr_val_ids] if len(self.attr_val_ids) > 0 else False if not prefix and not suffix: self.name = False elif prefix and suffix: self.name = '%s > %s' % (prefix, suffix) elif prefix: self.name = prefix else: self.name = suffix # -- Apply template @api.multi def apply_template(self): for rec in self: is_global = False if rec.category_id: # Local templates products = self.env['product.product'].search([('categ_id', '=', rec.category_id.id), ('type', '=', 'product')]) else: # Global template products = self.env['product.product'].search([('type', '=', 'product')]) is_global = True # Apply template to products for product in products: # Get existing rules existing_rules = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', product.id)]) len_existing_rules = len(existing_rules) # Do not apply Global template if rules exist if is_global and len_existing_rules > 0 and len(existing_rules.filtered(lambda t: not t.template_id)) == 0: continue vals = { 'product_id': product.id, 'template_id': rec.id, 'product_min_qty': rec.product_min_qty, 'product_max_qty': rec.product_max_qty, 'qty_multiple': rec.qty_multiple, 'group_id': rec.group_id.id, 'lead_days': rec.lead_days, 'lead_type': rec.lead_type, } if rec.warehouse_id: vals.update({'warehouse_id': rec.warehouse_id.id}) if rec.location_id: vals.update({'location_id': rec.location_id.id}) # Update existing reordering rules or create new if len_existing_rules > 0: existing_rules.filtered("template_control").write(vals) else: vals.update({'template_control': True}) self.env['stock.warehouse.orderpoint'].create(vals) # -- Create @api.model def create(self, vals): res = super(CXOrderpointTemplate, self).create(vals) res.apply_template() return res # -- Write @api.multi def write(self, vals): # Remove category and attribute values vals.pop('category_id', False) vals.pop('attr_val_ids', False) # Write res = super(CXOrderpointTemplate, self).write(vals) # Get new vals for rules rule_vals = {} for key in TEMPLATE_FIELDS: if key in vals: rule_vals.update({key: vals.get(key, False)}) # Update all related rules if len(rule_vals) > 0: self.env['stock.warehouse.orderpoint'].search([('template_id', 'in', self.ids)]).write(rule_vals) return res # -- Delete @api.multi def unlink(self): # Get products product_ids = self.mapped('rule_ids').mapped('product_id') # Delete templates res = super(CXOrderpointTemplate, self).unlink() # Re-apply template if len(product_ids) > 0: product_ids.reorder_rules_update() return res
class account_transfer(osv.osv): def _get_balance(self, src_journal, dst_journal, company): src_balance = dst_balance = 0.0 #import pdb; pdb.set_trace() if src_journal.default_credit_account_id.id == src_journal.default_debit_account_id.id: if not src_journal.currency or company.currency_id.id == src_journal.currency.id: src_balance = src_journal.default_credit_account_id.balance else: src_balance = src_journal.default_credit_account_id.foreign_balance else: if not src_journal.currency or company.currency_id.id == src_journal.currency.id: src_balance = src_journal.default_debit_account_id.balance - src_journal.default_credit_account_id.balance else: src_balance = src_journal.default_debit_account_id.foreign_balance - src_journal.default_credit_account_id.foreign_balance if dst_journal.default_credit_account_id.id == dst_journal.default_debit_account_id.id: if not dst_journal.currency or company.currency_id.id == dst_journal.currency.id: dst_balance = dst_journal.default_credit_account_id.balance else: dst_balance = dst_journal.default_credit_account_id.foreign_balance else: if not dst_journal.currency or company.currency_id.id == dst_journal.currency.id: dst_balance = dst_journal.default_debit_account_id.balance - dst_journal.default_credit_account_id.balance else: dst_balance = dst_journal.default_debit_account_id.foreign_balance - dst_journal.default_credit_account_id.foreign_balance return (src_balance, dst_balance) def _balance(self, cr, uid, ids, field_name, arg, context=None): res = {} for trans in self.browse(cr, uid, ids, context=context): src_balance, dst_balance = self._get_balance( trans.src_journal_id, trans.dst_journal_id, trans.company_id) exchange = False if trans.dst_journal_id.currency.id != trans.src_journal_id.currency.id: exchange = True res[trans.id] = { 'src_balance': src_balance, 'dst_balance': dst_balance, 'exchange': exchange, 'exchange_inv': (trans.exchange_rate and 1.0 / trans.exchange_rate or 0.0) } return res def _get_type(self, cr, uid, context=None): return [('transfer', 'Transfer')] STATE_SELECTION = [ ('draft', 'Draft'), ('confirm', 'Confirm'), ('done', 'Done'), ('cancel', 'Cancel'), ] _columns = { 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'type': fields.selection(_get_type, 'Type', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'name': fields.char('Number', size=32, required=True, readonly=True, states={'draft': [('readonly', False)]}), 'date': fields.date('Date', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'origin': fields.char('Origin', size=128, readonly=True, states={'draft': [('readonly', False)]}, help="Origin Document"), 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}), 'voucher_ids': fields.one2many('account.voucher', 'transfer_id', string='Payments', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }), 'src_journal_id': fields.many2one('account.journal', 'Source Journal', required=True, domain=[('type', 'in', ['cash', 'bank'])], select=True, readonly=True, states={'draft': [('readonly', False)]}), 'src_partner_id': fields.many2one('res.partner', 'Source Partner', select=True), 'src_balance': fields.function( _balance, digits_compute=dp.get_precision('Account'), string='Current Source Balance', type='float', readonly=True, multi='balance', help="Include all account moves in draft and confirmed state"), 'src_amount': fields.float('Source Amount', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'src_have_partner': fields.related('src_journal_id', 'have_partner', type='boolean', string='Have Partner', readonly=True), 'dst_journal_id': fields.many2one('account.journal', 'Destinity Journal', required=True, domain=[('type', 'in', ['cash', 'bank'])], select=True, readonly=True, states={'draft': [('readonly', False)]}), 'dst_partner_id': fields.many2one('res.partner', 'Destinity Partner', select=True), 'dst_balance': fields.function( _balance, digits_compute=dp.get_precision('Account'), string='Current Destinity Balance', type='float', readonly=True, multi='balance', help="Include all account moves in draft and confirmed state"), 'dst_amount': fields.float('Destinity Amount', required=True, readonly=True, states={'draft': [('readonly', False)]}), 'dst_have_partner': fields.related('dst_journal_id', 'have_partner', type='boolean', string='Have Partner', readonly=True), 'exchange_rate': fields.float('Exchange Rate', digits_compute=dp.get_precision('Exchange'), readonly=True, states={'draft': [('readonly', False)]}), 'exchange': fields.function(_balance, string='Have Exchange', type='boolean', readonly=True, multi='balance'), 'exchange_inv': fields.function(_balance, string='1 / Exchange Rate', type='float', digits_compute=dp.get_precision('Exchange'), readonly=True, multi='balance'), 'adjust_move': fields.many2one( 'account.move', 'Adjust Move', readonly=True, help="Adjust move usually by difference in the money exchange"), 'state': fields.selection(STATE_SELECTION, string='State', track_visibility='onchange', readonly=True), } _defaults = { 'type': 'transfer', 'name': lambda s, cr, u, c: s.pool.get('ir.sequence').get( cr, u, 'account.transfer'), 'company_id': lambda s, cr, u, c: s.pool.get('res.users').browse(cr, u, u).company_id .id, 'date': lambda *a: time.strftime('%Y-%m-%d'), 'exchange_rate': 1.0, 'exchange_inv': 1.0, 'state': 'draft', } _sql_constraints = [('name_unique', 'unique(company_id,name)', _('The number must be unique!'))] _name = 'account.transfer' _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = 'Account Cash and Bank Transfer' _order = 'name desc' def voucher_get(self, cr, uid, trans, context=None): res = {} res['transfer_id'] = trans.id res['type'] = 'transfer' res['company_id'] = trans.company_id.id res['reference'] = trans.name + str(trans.origin and (' - ' + trans.origin) or '') res['line_ids'] = [(0, 0, {})] res['line_ids'][0][2][ 'account_analytic_id'] = trans.account_analytic_id and trans.account_analytic_id.id or 0 res['line_ids'][0][2]['name'] = trans.origin return res def unlink(self, cr, uid, ids, context=None): for trans in self.browse(cr, uid, ids, context=context): if trans.state not in ('draft'): raise osv.except_osv( _('User Error!'), _('You cannot delete a not draft transfer "%s"') % trans.name) return super(account_transfer, self).unlink(cr, uid, ids, context=context) def copy(self, cr, uid, id, defaults, context=None): defaults['name'] = self.pool.get('ir.sequence').get( cr, uid, 'account.transfer') defaults['voucher_ids'] = [] return super(account_transfer, self).copy(cr, uid, id, defaults, context=context) def onchange_amount(self, cr, uid, ids, field, src_amount, dst_amount, exchange_rate): res = {'value': {}} if field == 'src_amount': res['value']['src_amount'] = src_amount res['value']['dst_amount'] = src_amount * exchange_rate res['value']['exchange_rate'] = exchange_rate res['value'][ 'exchange_inv'] = exchange_rate and 1.0 / exchange_rate or 0.0 elif field == 'dst_amount': res['value'][ 'src_amount'] = exchange_rate and dst_amount / exchange_rate or 0.0 res['value']['dst_amount'] = dst_amount res['value']['exchange_rate'] = exchange_rate res['value'][ 'exchange_inv'] = exchange_rate and 1.0 / exchange_rate or 0.0 elif field == 'exchange_rate': res['value']['src_amount'] = src_amount res['value']['dst_amount'] = src_amount * exchange_rate res['value']['exchange_rate'] = exchange_rate res['value'][ 'exchange_inv'] = exchange_rate and 1.0 / exchange_rate or 0.0 return res def onchange_journal(self, cr, uid, ids, src_journal_id, dst_journal_id, date, exchange_rate, src_amount): res = {'value': {}} if not (src_journal_id and dst_journal_id): return res src_journal = self.pool.get('account.journal').browse( cr, uid, src_journal_id) dst_journal = self.pool.get('account.journal').browse( cr, uid, dst_journal_id) res['value']['src_balance'], res['value'][ 'dst_balance'] = self._get_balance(src_journal, dst_journal, src_journal.company_id) res['value']['exchange'] = (src_journal.currency.id != dst_journal.currency.id) res['value']['src_have_partner'], res['value'][ 'dst_have_partner'] = src_journal.have_partner, dst_journal.have_partner res['value']['exchange_rate'] = exchange_rate if res['value']['exchange']: res['value']['exchange_rate'] = ( src_journal.currency and src_journal.currency.rate or src_journal.company_id.currency_id.rate) and ( (dst_journal.currency and dst_journal.currency.rate or dst_journal.company_id.currency_id.rate) / (src_journal.currency and src_journal.currency.rate or src_journal.company_id.currency_id.rate)) or 0.0 else: res['value']['exchange_rate'] = 1.0 res['value']['exchange_inv'] = res['value']['exchange_rate'] and ( 1.0 / res['value']['exchange_rate']) or 0.0 res['value']['dst_amount'] = res['value']['exchange_rate'] * src_amount return res def action_confirm(self, cr, uid, ids, context=None): voucher_obj = self.pool.get('account.voucher') for trans in self.browse(cr, uid, ids, context=context): sval = self.voucher_get(cr, uid, trans, context=context) dval = self.voucher_get(cr, uid, trans, context=context) sval['journal_id'] = trans.src_journal_id.id dval['journal_id'] = trans.dst_journal_id.id sval[ 'account_id'] = trans.src_journal_id.default_credit_account_id.id dval[ 'account_id'] = trans.dst_journal_id.default_debit_account_id.id sval[ 'payment_rate'] = trans.src_journal_id.currency.id and trans.company_id.currency_id.id <> trans.src_journal_id.currency.id and trans.exchange_rate or 1.0 dval[ 'payment_rate'] = trans.dst_journal_id.currency.id and trans.company_id.currency_id.id <> trans.dst_journal_id.currency.id and trans.exchange_inv or 1.0 dval[ 'payment_rate_currency_id'] = trans.dst_journal_id.currency.id or trans.company_id.currency_id.id sval[ 'payment_rate_currency_id'] = trans.src_journal_id.currency.id or trans.company_id.currency_id.id #import pdb; pdb.set_trace() sval['line_ids'][0][2]['amount'] = sval[ 'amount'] = trans.src_amount dval['line_ids'][0][2]['amount'] = dval[ 'amount'] = trans.dst_amount sval['line_ids'][0][2]['type'] = 'dr' dval['line_ids'][0][2]['type'] = 'cr' sval['line_ids'][0][2][ 'account_id'] = trans.dst_journal_id.default_debit_account_id.id if trans.src_partner_id.id ^ trans.dst_partner_id.id: sval[ 'partner_id'] = trans.src_have_partner and trans.src_partner_id.id or trans.dst_partner_id.id else: sval[ 'partner_id'] = trans.src_have_partner and trans.src_partner_id.id or trans.company_id.partner_id.id dval[ 'partner_id'] = trans.dst_have_partner and trans.dst_partner_id.id or trans.company_id.partner_id.id sval['line_ids'][0][2]['account_id'] = dval['line_ids'][0][2][ 'account_id'] = trans.src_journal_id.account_transit.id voucher_obj.create(cr, uid, dval, context=context) voucher_obj.create(cr, uid, sval, context=context) return self.write(cr, uid, ids, {'state': 'confirm'}, context=context) def action_done(self, cr, uid, ids, context=None): voucher_obj = self.pool.get('account.voucher') move_obj = self.pool.get('account.move') for trans in self.browse(cr, uid, ids, context=context): paid_amount = [] for voucher in trans.voucher_ids: (voucher.state == 'draft') and voucher_obj.proforma_voucher( cr, uid, [voucher.id], context=context) sign = (voucher.journal_id.id == trans.src_journal_id.id) and 1 or -1 paid_amount.append( sign * voucher_obj._paid_amount_in_company_currency( cr, uid, [voucher.id], '', '')[voucher.id]) #paid_amount.append(sign * voucher.paid_amount_in_company_currency) sum_amount = sum(paid_amount) if len(paid_amount) > 1 and sum_amount != 0.0: periods = self.pool.get('account.period').find(cr, uid) move = {} move['journal_id'] = trans.dst_journal_id.id move['period_id'] = periods and periods[0] or False move['ref'] = trans.name + str(trans.origin and (' - ' + trans.origin) or '') move['date'] = trans.date move['line_id'] = [(0, 0, {}), (0, 0, {})] move['line_id'][0][2]['name'] = trans.name move['line_id'][1][2]['name'] = trans.name if sum_amount > 0: move['line_id'][0][2][ 'account_id'] = trans.dst_journal_id.default_debit_account_id.id move['line_id'][1][2][ 'account_id'] = trans.src_journal_id.account_transit.id #trans.company_id.income_currency_exchange_account_id.id move['line_id'][0][2]['debit'] = sum_amount move['line_id'][1][2]['credit'] = sum_amount else: move['line_id'][0][2][ 'account_id'] = trans.dst_journal_id.default_credit_account_id.id move['line_id'][1][2][ 'account_id'] = trans.src_journal_id.account_transit.id #trans.company_id.expense_currency_exchange_account_id.id move['line_id'][1][2]['debit'] = -1 * sum_amount move['line_id'][0][2]['credit'] = -1 * sum_amount move_id = move_obj.create(cr, uid, move, context=context) self.write(cr, uid, [trans.id], {'adjust_move': move_id}, context=context) return self.write(cr, uid, ids, {'state': 'done'}, context=context) def action_cancel(self, cr, uid, ids, context=None): voucher_obj = self.pool.get('account.voucher') move_obj = self.pool.get('account.move') #import pdb; pdb.set_trace() for trans in self.browse(cr, uid, ids, context=context): for voucher in trans.voucher_ids: voucher_obj.unlink(cr, uid, [voucher.id], context=context) trans.adjust_move and move_obj.unlink( cr, uid, [trans.adjust_move.id], context=context) return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) def action_draft(self, cr, uid, ids, context=None): self.write(cr, uid, ids, {'state': 'draft'}) # wf_service = netsvc.LocalService("workflow") # for trans_id in ids: # wf_service.trg_delete(uid, 'account.transfer', trans_id, cr) # wf_service.trg_create(uid, 'account.transfer', trans_id, cr) return True
class account_expense(osv.Model): _inherit = 'account.expense' _columns = { 'amount_planed': fields.float('Planed Amount', digits_compute=dp.get_precision('Account'), readonly=True, states={'draft': [('readonly', False)]}, help="Planed incomes to receive"), 'date_end': fields.date('Date End', readonly=True, states={'draft': [('readonly', False)]}), 'description': fields.html('Description', readonly=True, states={'draft': [('readonly', False)]}), 'property_bank_journal_id': fields.property( 'account.journal', type='many2one', view_load=True, relation='account.journal', string='Bank Journal', readonly=True, states={'draft': [('readonly', False)]}, help="Default bank journal to make the automated cash transfers"), } def action_pre_approve(self, cr, uid, ids, context=None): for expense in self.browse(cr, uid, ids, context=context): if not expense.invoice_ids: raise osv.except_osv( _('User Error!'), _('You must register almost one invoice on invoices tab')) return super(account_expense, self).action_pre_approve(cr, uid, ids, context=context) def action_approve(self, cr, uid, ids, context=None): for expense in self.browse(cr, uid, ids, context=context): if expense.type == 'expenditure' and not self.pool.get( 'res.currency').is_zero(cr, uid, expense.currency_id, expense.amount_balance): raise osv.except_osv( _('User Error!'), _('Te balance amount must be zero (current balance:%d)') % (expense.amount_balance)) return super(account_expense, self).action_approve(cr, uid, ids, context=context) def action_generate_transfer(self, cr, uid, ids, context=None): context = context or {} transfer_obj = self.pool.get('account.transfer') for expense in self.browse(cr, uid, ids, context=context): if not expense.property_bank_journal_id: raise openerp.exceptions.Warning( _("You must configure a new property (property bank journal_id) for the current company, on default bank journal!" )) amount = expense.amount_planed + expense.amount_transfer if amount <= 0.01: continue src_journal = expense.property_bank_journal_id dst_journal = expense.journal_id dst_partner_id = expense.partner_id.id value = transfer_obj.onchange_journal(cr, uid, ids, src_journal.id, dst_journal.id, time.strftime('%Y-%m-%d'), 1.0, amount)['value'] value.update( transfer_obj.onchange_amount(cr, uid, ids, 'dst_amount', 0.0, amount, value['exchange_rate'])['value']) transfer_obj.create(cr, uid, { 'company_id': expense.company_id.id, 'type': 'expense', 'origin': expense.name, 'src_journal_id': src_journal.id, 'src_amount': value['src_amount'], 'dst_journal_id': dst_journal.id, 'dst_partner_id': dst_partner_id, 'dst_amount': amount, 'exchange_rate': value['exchange_rate'], 'expense_id': expense.id, }, context=context) return super(account_expense, self).action_generate_transfer(cr, uid, ids, context=context)
class ProductProduct(models.Model): """ Add a field for the stock available to promise. Useful implementations need to be installed through the Settings menu or by installing one of the modules stock_available_* """ _inherit = 'product.product' @api.multi def _compute_available_quantities_dict(self): stock_dict = self._compute_quantities_dict( self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'), self._context.get('from_date'), self._context.get('to_date')) res = {} for product in self: res[product.id] = { 'immediately_usable_qty': stock_dict[product.id][ 'virtual_available'], 'potential_qty': 0.0 } return res, stock_dict @api.multi @api.depends('virtual_available') def _compute_available_quantities(self): res, _ = self._compute_available_quantities_dict() for product in self: for key, value in res[product.id].items(): if hasattr(product, key): product[key] = value immediately_usable_qty = fields.Float( digits=dp.get_precision('Product Unit of Measure'), compute='_compute_available_quantities', search="_search_immediately_usable_qty", string='Available to promise', help="Stock for this Product that can be safely proposed " "for sale to Customers.\n" "The definition of this value can be configured to suit " "your needs.") potential_qty = fields.Float( compute='_compute_available_quantities', digits=dp.get_precision('Product Unit of Measure'), string='Potential', help="Quantity of this Product that could be produced using " "the materials already at hand.") @api.model def _search_immediately_usable_qty(self, operator, value): """ Search function for the immediately_usable_qty field. The search is quite similar to the Odoo search about quantity available (addons/stock/models/product.py,253; _search_product_quantity function) :param operator: str :param value: str :return: list of tuple (domain) """ products = self.search([]) # Force prefetch products.mapped("immediately_usable_qty") product_ids = [] for product in products: if OPERATORS[operator](product.immediately_usable_qty, value): product_ids.append(product.id) return [('id', 'in', product_ids)]
class PurchaseCostDistributionExpense(models.Model): _name = "purchase.cost.distribution.expense" _description = "Purchase cost distribution expense" @api.multi @api.depends('distribution', 'distribution.cost_lines') def _get_imported_lines(self): for record in self: record.imported_lines = record.env[ 'purchase.cost.distribution.line'] record.imported_lines |= record.distribution.cost_lines distribution = fields.Many2one(comodel_name='purchase.cost.distribution', string='Cost distribution', index=True, ondelete="cascade", required=True) ref = fields.Char(string="Reference") type = fields.Many2one(comodel_name='purchase.expense.type', string='Expense type', index=True, ondelete="restrict") calculation_method = fields.Selection(string='Calculation method', related='type.calculation_method', readonly=True) imported_lines = fields.Many2many( comodel_name='purchase.cost.distribution.line', string='Imported lines', compute='_get_imported_lines') affected_lines = fields.Many2many( comodel_name='purchase.cost.distribution.line', column1="expense_id", relation="distribution_expense_aff_rel", column2="line_id", string='Affected lines', help="Put here specific lines that this expense is going to be " "distributed across. Leave it blank to use all imported lines.", domain="[('id', 'in', imported_lines)]") expense_amount = fields.Float(string='Expense amount', digits=dp.get_precision('Account'), required=True) invoice_line = fields.Many2one( comodel_name='account.invoice.line', string="Supplier invoice line", domain="[('invoice_id.type', '=', 'in_invoice')," "('invoice_id.state', 'in', ('open', 'paid'))]") invoice_id = fields.Many2one(comodel_name='account.invoice', string="Invoice") display_name = fields.Char(compute="_compute_display_name", store=True) company_id = fields.Many2one( comodel_name="res.company", related="distribution.company_id", store=True, ) @api.multi @api.depends('distribution', 'type', 'expense_amount', 'ref') def _compute_display_name(self): for record in self: record.display_name = "%s: %s - %s (%s)" % ( record.distribution.name, record.type.name, record.ref, formatLang(record.env, record.expense_amount, currency_obj=record.distribution.currency_id)) @api.onchange('type') def onchange_type(self): if self.type and self.type.default_amount: self.expense_amount = self.type.default_amount @api.onchange('invoice_line') def onchange_invoice_line(self): self.invoice_id = self.invoice_line.invoice_id.id self.expense_amount = self.invoice_line.price_subtotal @api.multi def button_duplicate(self): for expense in self: expense.copy()
class PurchaseCostDistributionLine(models.Model): _name = "purchase.cost.distribution.line" _description = "Purchase cost distribution Line" @api.multi @api.depends('product_price_unit', 'product_qty') def _compute_total_amount(self): for dist_line in self: dist_line.total_amount = dist_line.product_price_unit * \ dist_line.product_qty @api.multi @api.depends('product_id', 'product_qty') def _compute_total_weight(self): for dist_line in self: dist_line.total_weight = dist_line.product_weight * \ dist_line.product_qty @api.multi @api.depends('product_id', 'product_qty') def _compute_total_volume(self): for dist_line in self: dist_line.total_volume = dist_line.product_volume * \ dist_line.product_qty @api.multi @api.depends('expense_lines', 'expense_lines.cost_ratio') def _compute_cost_ratio(self): for dist_line in self: dist_line.cost_ratio = sum( [x.cost_ratio for x in dist_line.expense_lines]) @api.multi @api.depends('expense_lines', 'expense_lines.expense_amount') def _compute_expense_amount(self): for dist_line in self: dist_line.expense_amount = sum( [x.expense_amount for x in dist_line.expense_lines]) @api.multi @api.depends('standard_price_old', 'cost_ratio') def _compute_standard_price_new(self): for dist_line in self: dist_line.standard_price_new = dist_line.standard_price_old + \ dist_line.cost_ratio @api.multi @api.depends('distribution', 'distribution.name', 'picking_id', 'picking_id.name', 'product_id', 'product_id.display_name') def _compute_name(self): for dist_line in self: dist_line.name = "%s: %s / %s" % ( dist_line.distribution.name, dist_line.picking_id.name, dist_line.product_id.display_name, ) @api.multi @api.depends('move_id', 'move_id.product_id') def _compute_product_id(self): for dist_line in self: # Cannot be done via related # field due to strange bug in update chain dist_line.product_id = dist_line.move_id.product_id.id @api.multi @api.depends('move_id', 'move_id.product_qty') def _get_product_qty(self): for dist_line in self: # Cannot be done via related # field due to strange bug in update chain dist_line.product_qty = dist_line.move_id.product_qty @api.multi @api.depends('move_id') def _compute_standard_price_old(self): for dist_line in self: dist_line.standard_price_old = ( dist_line.move_id and dist_line.move_id._get_price_unit() or 0.0) name = fields.Char( string='Name', compute='_compute_name', store=True, ) distribution = fields.Many2one(comodel_name='purchase.cost.distribution', string='Cost distribution', ondelete='cascade', required=True) move_id = fields.Many2one(comodel_name='stock.move', string='Picking line', ondelete="restrict", required=True) purchase_line_id = fields.Many2one(comodel_name='purchase.order.line', string='Purchase order line', related='move_id.purchase_line_id') purchase_id = fields.Many2one(comodel_name='purchase.order', string='Purchase order', readonly=True, related='move_id.purchase_line_id.order_id', store=True) partner = fields.Many2one( comodel_name='res.partner', string='Supplier', readonly=True, related='move_id.purchase_line_id.order_id.partner_id') picking_id = fields.Many2one('stock.picking', string='Picking', related='move_id.picking_id', store=True) product_id = fields.Many2one(comodel_name='product.product', string='Product', store=True, compute='_compute_product_id') product_qty = fields.Float(string='Quantity', compute='_get_product_qty', store=True) product_uom = fields.Many2one(comodel_name='product.uom', string='Unit of measure', related='move_id.product_uom') product_price_unit = fields.Float(string='Unit price', related='move_id.price_unit') expense_lines = fields.One2many( comodel_name='purchase.cost.distribution.line.expense', inverse_name='distribution_line', string='Expenses distribution lines') product_volume = fields.Float(string='Volume', help="The volume in m3.", related='product_id.product_tmpl_id.volume') product_weight = fields.Float(string='Gross weight', related='product_id.product_tmpl_id.weight', help="The gross weight in Kg.") standard_price_old = fields.Float(string='Previous cost', compute="_compute_standard_price_old", store=True, digits=dp.get_precision('Product Price')) expense_amount = fields.Float(string='Cost amount', digits=dp.get_precision('Account'), compute='_compute_expense_amount') cost_ratio = fields.Float(string='Unit cost', compute='_compute_cost_ratio') standard_price_new = fields.Float(string='New cost', digits=dp.get_precision('Product Price'), compute='_compute_standard_price_new') total_amount = fields.Float(compute=_compute_total_amount, string='Amount line', digits=dp.get_precision('Account')) total_weight = fields.Float(compute=_compute_total_weight, string="Line weight", store=True, digits=dp.get_precision('Stock Weight'), help="The line gross weight in Kg.") total_volume = fields.Float(compute=_compute_total_volume, string='Line volume', store=True, help="The line volume in m3.") company_id = fields.Many2one( comodel_name="res.company", related="distribution.company_id", store=True, )
class PurchaseCostDistribution(models.Model): _name = "purchase.cost.distribution" _description = "Purchase landed costs distribution" _order = 'name desc' @api.multi @api.depends('total_expense', 'total_purchase') def _compute_amount_total(self): for distribution in self: distribution.amount_total = distribution.total_purchase + \ distribution.total_expense @api.multi @api.depends('cost_lines', 'cost_lines.total_amount') def _compute_total_purchase(self): for distribution in self: distribution.total_purchase = sum( [x.total_amount for x in distribution.cost_lines]) @api.multi @api.depends('cost_lines', 'cost_lines.product_price_unit') def _compute_total_price_unit(self): for distribution in self: distribution.total_price_unit = sum( [x.product_price_unit for x in distribution.cost_lines]) @api.multi @api.depends('cost_lines', 'cost_lines.product_qty') def _compute_total_uom_qty(self): for distribution in self: distribution.total_uom_qty = sum( [x.product_qty for x in distribution.cost_lines]) @api.multi @api.depends('cost_lines', 'cost_lines.total_weight') def _compute_total_weight(self): for distribution in self: distribution.total_weight = sum( [x.total_weight for x in distribution.cost_lines]) @api.multi @api.depends('cost_lines', 'cost_lines.total_volume') def _compute_total_volume(self): for distribution in self: distribution.total_volume = sum( [x.total_volume for x in distribution.cost_lines]) @api.multi @api.depends('expense_lines', 'expense_lines.expense_amount') def _compute_total_expense(self): for distribution in self: distribution.total_expense = sum( [x.expense_amount for x in distribution.expense_lines]) def _expense_lines_default(self): expenses = self.env['purchase.expense.type'].search([ ('default_expense', '=', True) ]) return [{ 'type': x, 'expense_amount': x.default_amount } for x in expenses] name = fields.Char(string='Distribution number', required=True, index=True, default='/') company_id = fields.Many2one( comodel_name='res.company', string='Company', required=True, default=(lambda self: self.env['res.company']._company_default_get( 'purchase.cost.distribution'))) currency_id = fields.Many2one(comodel_name='res.currency', string='Currency', related="company_id.currency_id") state = fields.Selection([('draft', 'Draft'), ('calculated', 'Calculated'), ('done', 'Done'), ('error', 'Error'), ('cancel', 'Cancel')], string='Status', readonly=True, default='draft') cost_update_type = fields.Selection([('direct', 'Direct Update')], string='Cost Update Type', default='direct', required=True) date = fields.Date(string='Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today) total_uom_qty = fields.Float(compute=_compute_total_uom_qty, readonly=True, digits=dp.get_precision('Product UoS'), string='Total quantity') total_weight = fields.Float(compute=_compute_total_weight, string='Total gross weight', readonly=True, digits=dp.get_precision('Stock Weight')) total_volume = fields.Float(compute=_compute_total_volume, string='Total volume', readonly=True) total_purchase = fields.Float(compute=_compute_total_purchase, digits=dp.get_precision('Account'), string='Total purchase') total_price_unit = fields.Float(compute=_compute_total_price_unit, string='Total price unit', digits=dp.get_precision('Product Price')) amount_total = fields.Float(compute=_compute_amount_total, digits=dp.get_precision('Account'), string='Total') total_expense = fields.Float(compute=_compute_total_expense, digits=dp.get_precision('Account'), string='Total expenses') note = fields.Text(string='Documentation for this order') cost_lines = fields.One2many( comodel_name='purchase.cost.distribution.line', ondelete="cascade", inverse_name='distribution', string='Distribution lines') expense_lines = fields.One2many( comodel_name='purchase.cost.distribution.expense', ondelete="cascade", inverse_name='distribution', string='Expenses', default=_expense_lines_default) @api.multi def unlink(self): for record in self: if record.state not in ('draft', 'calculated'): raise UserError( _("You can't delete a confirmed cost distribution")) return super(PurchaseCostDistribution, self).unlink() @api.model def create(self, vals): if vals.get('name', '/') == '/': vals['name'] = self.env['ir.sequence'].next_by_code( 'purchase.cost.distribution') return super(PurchaseCostDistribution, self).create(vals) @api.multi def write(self, vals): for command in vals.get('cost_lines', []): if command[0] in (2, 3, 5): if command[0] == 5: to_check = self.mapped('cost_lines').ids else: to_check = [command[1]] lines = self.mapped('expense_lines.affected_lines').ids if any(i in lines for i in to_check): raise UserError( _("You can't delete a cost line if it's an " "affected line of any expense line.")) return super(PurchaseCostDistribution, self).write(vals) @api.model def _prepare_expense_line(self, expense_line, cost_line): distribution = cost_line.distribution if expense_line.type.calculation_method == 'amount': multiplier = cost_line.total_amount if expense_line.affected_lines: divisor = sum( [x.total_amount for x in expense_line.affected_lines]) else: divisor = distribution.total_purchase elif expense_line.type.calculation_method == 'price': multiplier = cost_line.product_price_unit if expense_line.affected_lines: divisor = sum([ x.product_price_unit for x in expense_line.affected_lines ]) else: divisor = distribution.total_price_unit elif expense_line.type.calculation_method == 'qty': multiplier = cost_line.product_qty if expense_line.affected_lines: divisor = sum( [x.product_qty for x in expense_line.affected_lines]) else: divisor = distribution.total_uom_qty elif expense_line.type.calculation_method == 'weight': multiplier = cost_line.total_weight if expense_line.affected_lines: divisor = sum( [x.total_weight for x in expense_line.affected_lines]) else: divisor = distribution.total_weight elif expense_line.type.calculation_method == 'volume': multiplier = cost_line.total_volume if expense_line.affected_lines: divisor = sum( [x.total_volume for x in expense_line.affected_lines]) else: divisor = distribution.total_volume elif expense_line.type.calculation_method == 'equal': multiplier = 1 divisor = (len(expense_line.affected_lines) or len(distribution.cost_lines)) else: raise UserError(_('No valid distribution type.')) if divisor: expense_amount = (expense_line.expense_amount * multiplier / divisor) else: raise UserError( _("The cost for the line '%s' can't be " "distributed because the calculation method " "doesn't provide valid data" % expense_line.type.name)) return { 'distribution_expense': expense_line.id, 'expense_amount': expense_amount, 'cost_ratio': expense_amount / cost_line.product_qty, } @api.multi def action_calculate(self): for distribution in self: # Check expense lines for amount 0 if any([not x.expense_amount for x in distribution.expense_lines]): raise UserError( _('Please enter an amount for all the expenses')) # Check if exist lines in distribution if not distribution.cost_lines: raise UserError( _('There is no picking lines in the distribution')) # Calculating expense line for cost_line in distribution.cost_lines: cost_line.expense_lines.unlink() expense_lines = [] for expense in distribution.expense_lines: if (expense.affected_lines and cost_line not in expense.affected_lines): continue expense_lines.append( self._prepare_expense_line(expense, cost_line)) cost_line.expense_lines = [(0, 0, x) for x in expense_lines] distribution.state = 'calculated' return True def _product_price_update(self, product, vals_list): """Method that mimicks stock.move's product_price_update_before_done method behaviour, but taking into account that calculations are made on an already done moves, and prices sources are given as parameters. """ moves_total_qty = 0 moves_total_diff_price = 0 for move, price_diff in vals_list: moves_total_qty += move.product_qty moves_total_diff_price += move.product_qty * price_diff prev_qty_available = product.qty_available - moves_total_qty if prev_qty_available <= 0: prev_qty_available = 0 total_available = prev_qty_available + moves_total_qty new_std_price = ((total_available * product.standard_price + moves_total_diff_price) / total_available) # Write the standard price, as SUPERUSER_ID, because a # warehouse manager may not have the right to write on products product.sudo().write({'standard_price': new_std_price}) @api.multi def action_done(self): """Perform all moves that touch the same product in batch.""" self.ensure_one() if self.cost_update_type != 'direct': return d = {} for line in self.cost_lines: product = line.move_id.product_id if (product.cost_method != 'average' or line.move_id.location_id.usage != 'supplier'): continue d.setdefault(product, []) d[product].append( (line.move_id, line.standard_price_new - line.standard_price_old), ) for product, vals_list in d.items(): self._product_price_update(product, vals_list) for move, price_diff in vals_list: move.price_unit += price_diff move.value = move.product_uom_qty * move.price_unit self.state = 'done' @api.multi def action_draft(self): self.write({'state': 'draft'}) return True @api.multi def action_cancel(self): """Perform all moves that touch the same product in batch.""" self.ensure_one() self.state = 'draft' if self.cost_update_type != 'direct': return d = {} for line in self.cost_lines: product = line.move_id.product_id if (product.cost_method != 'average' or line.move_id.location_id.usage != 'supplier'): continue if self.currency_id.compare_amounts(line.move_id.price_unit, line.standard_price_new) != 0: raise UserError( _('Cost update cannot be undone because there has ' 'been a later update. Restore correct price and try ' 'again.')) d.setdefault(product, []) d[product].append( (line.move_id, line.standard_price_old - line.standard_price_new), ) for product, vals_list in d.items(): self._product_price_update(product, vals_list) for move, price_diff in vals_list: move.price_unit += price_diff move._run_valuation()
class StockRequest(models.AbstractModel): _name = "stock.request.abstract" _description = "Stock Request Template" _inherit = ['mail.thread', 'mail.activity.mixin'] @api.model def default_get(self, fields): res = super(StockRequest, self).default_get(fields) warehouse = None if 'warehouse_id' not in res and res.get('company_id'): warehouse = self.env['stock.warehouse'].search( [('company_id', '=', res['company_id'])], limit=1) if warehouse: res['warehouse_id'] = warehouse.id res['location_id'] = warehouse.lot_stock_id.id return res @api.depends('product_id', 'product_uom_id', 'product_uom_qty', 'product_id.product_tmpl_id.uom_id') def _compute_product_qty(self): for rec in self: rec.product_qty = rec.product_uom_id._compute_quantity( rec.product_uom_qty, rec.product_id.product_tmpl_id.uom_id) name = fields.Char('Name', copy=False, required=True, readonly=True, default='/') warehouse_id = fields.Many2one( 'stock.warehouse', 'Warehouse', ondelete="cascade", required=True, ) location_id = fields.Many2one( 'stock.location', 'Location', domain=[('usage', 'in', ['internal', 'transit'])], ondelete="cascade", required=True, ) product_id = fields.Many2one( 'product.product', 'Product', domain=[('type', 'in', ['product', 'consu'])], ondelete='cascade', required=True, ) allow_virtual_location = fields.Boolean( related='company_id.stock_request_allow_virtual_loc', readonly=True, ) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', required=True, default=lambda self: self._context.get('product_uom_id', False), ) product_uom_qty = fields.Float( 'Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, help="Quantity, specified in the unit of measure indicated in the " "request.", ) product_qty = fields.Float( 'Real Quantity', compute='_compute_product_qty', store=True, copy=False, digits=dp.get_precision('Product Unit of Measure'), help='Quantity in the default UoM of the product', ) procurement_group_id = fields.Many2one( 'procurement.group', 'Procurement Group', help="Moves created through this stock request will be put in this " "procurement group. If none is given, the moves generated by " "procurement rules will be grouped into one big picking.", ) company_id = fields.Many2one( 'res.company', 'Company', required=True, default=lambda self: self.env['res.company']._company_default_get( 'stock.request'), ) route_id = fields.Many2one('stock.location.route', string='Route', domain="[('id', 'in', route_ids)]", ondelete='restrict') route_ids = fields.Many2many( 'stock.location.route', string='Route', compute='_compute_route_ids', readonly=True, ) _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Name must be unique'), ] @api.depends('product_id', 'warehouse_id', 'location_id') def _compute_route_ids(self): route_obj = self.env['stock.location.route'] for wh in self.mapped('warehouse_id'): wh_routes = route_obj.search([('warehouse_ids', '=', wh.id)]) for record in self.filtered(lambda r: r.warehouse_id == wh): routes = route_obj if record.product_id: routes += record.product_id.mapped( 'route_ids') | record.product_id.mapped( 'categ_id').mapped('total_route_ids') if record.warehouse_id: routes |= wh_routes parents = record.get_parents().ids record.route_ids = routes.filtered(lambda r: any( p.location_id.id in parents for p in r.pull_ids)) def get_parents(self): location = self.location_id result = location while location.location_id: location = location.location_id result |= location return result @api.constrains('company_id', 'product_id', 'warehouse_id', 'location_id', 'route_id') def _check_company_constrains(self): """ Check if the related models have the same company """ for rec in self: if rec.product_id.company_id and \ rec.product_id.company_id != rec.company_id: raise ValidationError( _('You have entered a product that is assigned ' 'to another company.')) if rec.location_id.company_id and \ rec.location_id.company_id != rec.company_id: raise ValidationError( _('You have entered a location that is ' 'assigned to another company.')) if rec.warehouse_id.company_id != rec.company_id: raise ValidationError( _('You have entered a warehouse that is ' 'assigned to another company.')) if rec.route_id and rec.route_id.company_id and \ rec.route_id.company_id != rec.company_id: raise ValidationError( _('You have entered a route that is ' 'assigned to another company.')) @api.constrains('product_id') def _check_product_uom(self): ''' Check if the UoM has the same category as the product standard UoM ''' if any(request.product_id.uom_id.category_id != request.product_uom_id.category_id for request in self): raise ValidationError( _('You have to select a product unit of measure in the ' 'same category than the default unit ' 'of measure of the product')) @api.onchange('warehouse_id') def onchange_warehouse_id(self): """ Finds location id for changed warehouse. """ res = {'domain': {}} if self._name == 'stock.request' and self.order_id: # When the stock request is created from an order the wh and # location are taken from the order and we rely on it to change # all request associated. Thus, no need to apply # the onchange, as it could lead to inconsistencies. return res if self.warehouse_id: loc_wh = self.location_id.sudo().get_warehouse() if self.warehouse_id != loc_wh: self.location_id = self.warehouse_id.lot_stock_id.id if self.warehouse_id.company_id != self.company_id: self.company_id = self.warehouse_id.company_id return res @api.onchange('location_id') def onchange_location_id(self): if self.location_id: loc_wh = self.location_id.sudo().get_warehouse() if loc_wh and self.warehouse_id != loc_wh: self.warehouse_id = loc_wh self.with_context( no_change_childs=True).onchange_warehouse_id() @api.onchange('allow_virtual_location') def onchange_allow_virtual_location(self): if self.allow_virtual_location: return {'domain': {'location_id': []}} @api.onchange('company_id') def onchange_company_id(self): """ Sets a default warehouse when the company is changed and limits the user selection of warehouses. """ if self.company_id and ( not self.warehouse_id or self.warehouse_id.company_id != self.company_id): self.warehouse_id = self.env['stock.warehouse'].search( [('company_id', '=', self.company_id.id)], limit=1) self.onchange_warehouse_id() return { 'domain': { 'warehouse_id': [('company_id', '=', self.company_id.id)] } } @api.onchange('product_id') def onchange_product_id(self): res = {'domain': {}} if self.product_id: self.product_uom_id = self.product_id.uom_id.id res['domain']['product_uom_id'] = [ ('category_id', '=', self.product_id.uom_id.category_id.id) ] return res res['domain']['product_uom_id'] = [] return res
class AssignManualQuantsLines(models.TransientModel): _name = 'assign.manual.quants.lines' _rec_name = 'quant_id' assign_wizard = fields.Many2one(comodel_name='assign.manual.quants', string='Move', required=True, ondelete='cascade') quant_id = fields.Many2one(comodel_name='stock.quant', string='Quant', required=True, ondelete='cascade', oldname='quant') location_id = fields.Many2one(comodel_name='stock.location', string='Location', related='quant_id.location_id', readonly=True, groups="stock.group_stock_multi_locations") lot_id = fields.Many2one(comodel_name='stock.production.lot', string='Lot', related='quant_id.lot_id', readonly=True, groups="stock.group_production_lot") package_id = fields.Many2one(comodel_name='stock.quant.package', string='Package', related='quant_id.package_id', readonly=True, groups="stock.group_tracking_lot") owner_id = fields.Many2one( comodel_name='res.partner', string='Owner', related='quant_id.owner_id', readonly=True, groups="stock.group_tracking_owner", ) # This is not correctly shown as related or computed, so we make it regular on_hand = fields.Float( readonly=True, string='On Hand', digits=dp.get_precision('Product Unit of Measure'), ) reserved = fields.Float(string='Others Reserved', digits=dp.get_precision('Product Unit of Measure')) selected = fields.Boolean(string='Select') qty = fields.Float(string='QTY', digits=dp.get_precision('Product Unit of Measure')) @api.onchange('selected') def _onchange_selected(self): for record in self: if not record.selected: record.qty = 0 elif not record.qty: # This takes current "snapshot" situation, so that we don't # have to compute each time if current reserved quantity is # for this current move. If other operations change available # quantity on quant, a constraint would be raised later on # validation. quant_qty = record.on_hand - record.reserved remaining_qty = record.assign_wizard.move_qty record.qty = min(quant_qty, remaining_qty) @api.multi @api.constrains('qty') def _check_qty(self): precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for record in self.filtered('qty'): quant = record.quant_id move_lines = record.assign_wizard.move_id.move_line_ids.filtered( lambda ml: (ml.location_id == quant.location_id and ml.lot_id == quant.lot_id)) reserved = (quant.reserved_quantity - sum(move_lines.mapped('ordered_qty'))) if float_compare(record.qty, record.quant_id.quantity - reserved, precision_digits=precision_digits) > 0: raise UserError( _('Selected line quantity is higher than the available ' 'one. Maybe an operation with this product has been ' 'done meanwhile or you have manually increased the ' 'suggested value.')) def _assign_quant_line(self): self.ensure_one() quant = self.env['stock.quant'] precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') move = self.assign_wizard.move_id if float_compare(self.qty, 0.0, precision_digits=precision_digits) > 0: available_quantity = quant._get_available_quantity( move.product_id, self.location_id, lot_id=self.lot_id, package_id=self.package_id, owner_id=self.owner_id, ) if float_compare(available_quantity, 0.0, precision_digits=precision_digits) <= 0: return move._update_reserved_quantity(self.qty, available_quantity, self.location_id, lot_id=self.lot_id, package_id=self.package_id, owner_id=self.owner_id, strict=True)
class AssignManualQuants(models.TransientModel): _name = 'assign.manual.quants' @api.multi @api.constrains('quants_lines') def _check_qty(self): precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for record in self.filtered('quants_lines'): if float_compare(record.lines_qty, record.move_id.product_qty, precision_digits=precision_digits) > 0: raise UserError(_('Quantity is higher than the needed one')) @api.depends('move_id', 'quants_lines', 'quants_lines.qty') def _compute_qties(self): for record in self: record.lines_qty = sum( record.quants_lines.filtered('selected').mapped('qty')) record.move_qty = record.move_id.product_qty - record.lines_qty lines_qty = fields.Float( string='Reserved qty', compute='_compute_qties', digits=dp.get_precision('Product Unit of Measure')) move_qty = fields.Float(string='Remaining qty', compute='_compute_qties', digits=dp.get_precision('Product Unit of Measure')) quants_lines = fields.One2many('assign.manual.quants.lines', 'assign_wizard', string='Quants') move_id = fields.Many2one( comodel_name='stock.move', string='Move', ) @api.multi def assign_quants(self): move = self.move_id move._do_unreserve() for line in self.quants_lines: line._assign_quant_line() # Auto-fill all lines as done for ml in move.move_line_ids: ml.qty_done = ml.product_qty move._recompute_state() move.mapped('picking_id')._compute_state() return {} @api.model def default_get(self, fields): res = super(AssignManualQuants, self).default_get(fields) move = self.env['stock.move'].browse(self.env.context['active_id']) available_quants = self.env['stock.quant'].search([ ('location_id', 'child_of', move.location_id.id), ('product_id', '=', move.product_id.id), ('quantity', '>', 0), ]) quants_lines = [] for quant in available_quants: line = {} line['quant_id'] = quant.id line['on_hand'] = quant.quantity line['location_id'] = quant.location_id.id line['lot_id'] = quant.lot_id.id line['package_id'] = quant.package_id.id line['owner_id'] = quant.owner_id.id line['selected'] = False move_lines = move.move_line_ids.filtered( lambda ml: (ml.location_id == quant.location_id and ml.lot_id == quant.lot_id and ml.owner_id == quant.owner_id and ml.package_id == quant.package_id)) line['qty'] = sum(move_lines.mapped('ordered_qty')) line['selected'] = bool(line['qty']) line['reserved'] = quant.reserved_quantity - line['qty'] quants_lines.append(line) res.update({ 'quants_lines': [(0, 0, x) for x in quants_lines], 'move_id': move.id, }) return res
# Copyright 2018 Camptocamp SA # Copyright 2016 ACSONE SA/NV (<http://acsone.eu>) # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models, _ from addons import decimal_precision as dp from odoo.addons.stock.models.product import OPERATORS from odoo.tools.float_utils import float_round from odoo.exceptions import UserError UNIT = dp.get_precision('Product Unit of Measure') class ProductTemplate(models.Model): _inherit = "product.template" qty_available_not_res = fields.Float( string='Quantity On Hand Unreserved', digits=UNIT, compute='_compute_product_available_not_res', search='_search_quantity_unreserved', ) @api.multi @api.depends('product_variant_ids.qty_available_not_res') def _compute_product_available_not_res(self): for tmpl in self: if isinstance(tmpl.id, models.NewId): continue
class PurchaseRequestLine(models.Model): _name = "purchase.request.line" _description = "Purchase Request Line" _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char('Description', track_visibility='onchange') product_uom_id = fields.Many2one('product.uom', 'Product Unit of Measure', track_visibility='onchange') product_qty = fields.Float('Quantity', track_visibility='onchange', digits=dp.get_precision( 'Product Unit of Measure')) request_id = fields.Many2one('purchase.request', 'Purchase Request', ondelete='cascade', readonly=True) company_id = fields.Many2one('res.company', related='request_id.company_id', string='Company', store=True, readonly=True) analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', track_visibility='onchange') requested_by = fields.Many2one('res.users', related='request_id.requested_by', string='Requested by', store=True, readonly=True) assigned_to = fields.Many2one('res.users', related='request_id.assigned_to', string='Assigned to', store=True, readonly=True) date_start = fields.Date(related='request_id.date_start', string='Request Date', readonly=True, store=True) description = fields.Text(related='request_id.description', string='Description', readonly=True, store=True) origin = fields.Char(related='request_id.origin', string='Source Document', readonly=True, store=True) date_required = fields.Date(string='Request Date', required=True, track_visibility='onchange', default=fields.Date.context_today) is_editable = fields.Boolean(string='Is editable', compute="_compute_is_editable", readonly=True) specifications = fields.Text(string='Specifications') request_state = fields.Selection(string='Request state', readonly=True, related='request_id.state', selection=_STATES, store=True) supplier_id = fields.Many2one('res.partner', string='Preferred supplier', compute="_compute_supplier_id") cancelled = fields.Boolean( string="Cancelled", readonly=True, default=False, copy=False) purchased_qty = fields.Float(string='Quantity in RFQ or PO', compute="_compute_purchased_qty") purchase_lines = fields.Many2many( 'purchase.order.line', 'purchase_request_purchase_order_line_rel', 'purchase_request_line_id', 'purchase_order_line_id', 'Purchase Order Lines', readonly=True, copy=False) purchase_state = fields.Selection( compute="_compute_purchase_state", string="Purchase Status", selection=lambda self: self.env['purchase.order']._fields['state'].selection, store=True, ) move_dest_ids = fields.One2many('stock.move', 'created_purchase_request_line_id', 'Downstream Moves') orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint') estimated_cost = fields.Monetary( string='Estimated Cost', currency_field='currency_id', default=0.0, help='Estimated cost of Purchase Request Line, not propagated to PO.') currency_id = fields.Many2one( related='company_id.currency_id', readonly=True, ) @api.multi @api.depends('product_id', 'name', 'product_uom_id', 'product_qty', 'analytic_account_id', 'date_required', 'specifications', 'purchase_lines') def _compute_is_editable(self): for rec in self: if rec.request_id.state in ('to_approve', 'approved', 'rejected', 'done'): rec.is_editable = False else: rec.is_editable = True for rec in self.filtered(lambda p: p.purchase_lines): rec.is_editable = False @api.multi def _compute_supplier_id(self): for rec in self: if rec.product_id: if rec.product_id.seller_ids: rec.supplier_id = rec.product_id.seller_ids[0].name product_id = fields.Many2one( 'product.product', 'Product', domain=[('purchase_ok', '=', True)], track_visibility='onchange') @api.onchange('product_id') def onchange_product_id(self): if self.product_id: name = self.product_id.name if self.product_id.code: name = '[%s] %s' % (name, self.product_id.code) if self.product_id.description_purchase: name += '\n' + self.product_id.description_purchase self.product_uom_id = self.product_id.uom_id.id self.product_qty = 1 self.name = name @api.multi def do_cancel(self): """Actions to perform when cancelling a purchase request line.""" self.write({'cancelled': True}) @api.multi def do_uncancel(self): """Actions to perform when uncancelling a purchase request line.""" self.write({'cancelled': False}) @api.multi def write(self, vals): res = super(PurchaseRequestLine, self).write(vals) if vals.get('cancelled'): requests = self.mapped('request_id') requests.check_auto_reject() return res @api.multi def _compute_purchased_qty(self): for rec in self: rec.purchased_qty = 0.0 for line in rec.purchase_lines.filtered( lambda x: x.state != 'cancel'): if rec.product_uom_id and\ line.product_uom != rec.product_uom_id: rec.purchased_qty += line.product_uom._compute_quantity( line.product_qty, rec.product_uom_id) else: rec.purchased_qty += line.product_qty @api.multi @api.depends('purchase_lines.state', 'purchase_lines.order_id.state') def _compute_purchase_state(self): for rec in self: temp_purchase_state = False if rec.purchase_lines: if any([po_line.state == 'done' for po_line in rec.purchase_lines]): temp_purchase_state = 'done' elif all([po_line.state == 'cancel' for po_line in rec.purchase_lines]): temp_purchase_state = 'cancel' elif any([po_line.state == 'purchase' for po_line in rec.purchase_lines]): temp_purchase_state = 'purchase' elif any([po_line.state == 'to approve' for po_line in rec.purchase_lines]): temp_purchase_state = 'to approve' elif any([po_line.state == 'sent' for po_line in rec.purchase_lines]): temp_purchase_state = 'sent' elif all([po_line.state in ('draft', 'cancel') for po_line in rec.purchase_lines]): temp_purchase_state = 'draft' rec.purchase_state = temp_purchase_state @api.model def _planned_date(self, request_line, delay=0.0): company = request_line.company_id date_planned = datetime.strptime( request_line.date_required, '%Y-%m-%d') - \ relativedelta(days=company.po_lead) if delay: date_planned -= relativedelta(days=delay) return date_planned and date_planned.strftime('%Y-%m-%d') \ or False @api.model def _get_supplier_min_qty(self, product, partner_id=False): seller_min_qty = 0.0 if partner_id: seller = product.seller_ids \ .filtered(lambda r: r.name == partner_id) \ .sorted(key=lambda r: r.min_qty) else: seller = product.seller_ids.sorted(key=lambda r: r.min_qty) if seller: seller_min_qty = seller[0].min_qty return seller_min_qty @api.model def _calc_new_qty(self, request_line, po_line=None, new_pr_line=False): purchase_uom = po_line.product_uom or request_line.product_id.uom_po_id uom = request_line.product_uom_id qty = uom._compute_quantity(request_line.product_qty, purchase_uom) # Make sure we use the minimum quantity of the partner corresponding # to the PO. This does not apply in case of dropshipping supplierinfo_min_qty = 0.0 if not po_line.order_id.dest_address_id: supplierinfo_min_qty = self._get_supplier_min_qty( po_line.product_id, po_line.order_id.partner_id) rl_qty = 0.0 # Recompute quantity by adding existing running procurements. for rl in po_line.purchase_request_lines: rl_qty += rl.product_uom_id._compute_quantity( rl.product_qty, purchase_uom) qty = max(rl_qty, supplierinfo_min_qty) return qty @api.model def _calc_new_qty_purchase_requisition(self, request_line, po_line=None, new_pr_line=False): purchase_uom = po_line.product_uom_id or request_line.product_id.uom_po_id uom = request_line.product_uom_id qty = uom._compute_quantity(request_line.product_qty, purchase_uom) # Make sure we use the minimum quantity of the partner corresponding # to the PO. supplierinfo_min_qty = 0.0 supplierinfo_min_qty = self._get_supplier_min_qty( po_line.product_id, po_line.requisition_id.vendor_id) rl_qty = 0.0 # Recompute quantity by adding existing running procurements. for rl in po_line.purchase_request_lines: rl_qty += rl.product_uom_id._compute_quantity( rl.product_qty, purchase_uom) qty = max(rl_qty, supplierinfo_min_qty) return qty @api.multi def unlink(self): if self.mapped('purchase_lines'): raise UserError( _('You cannot delete a record that refers to purchase ' 'lines!')) return super(PurchaseRequestLine, self).unlink()