diff --git a/bahmni_product/views/product_supplierinfo_view.xml b/bahmni_product/views/product_supplierinfo_view.xml index 3cd1aff..960308b 100644 --- a/bahmni_product/views/product_supplierinfo_view.xml +++ b/bahmni_product/views/product_supplierinfo_view.xml @@ -6,7 +6,7 @@ - @@ -17,4 +17,14 @@ + + product.supplierinfo.with.mrp + product.supplierinfo + + + + + + + diff --git a/bahmni_purchase/__manifest__.py b/bahmni_purchase/__manifest__.py index 4f45c83..6eed034 100644 --- a/bahmni_purchase/__manifest__.py +++ b/bahmni_purchase/__manifest__.py @@ -15,6 +15,7 @@ 'data': ['security/ir.model.access.csv', 'views/purchase_views.xml', 'views/product_view.xml', + 'views/res_config_inherit.xml', 'views/price_markup_table_view.xml'], 'demo': [], 'qweb': [], diff --git a/bahmni_purchase/models/__init__.py b/bahmni_purchase/models/__init__.py index 406c41e..5ec8d10 100644 --- a/bahmni_purchase/models/__init__.py +++ b/bahmni_purchase/models/__init__.py @@ -1,3 +1,5 @@ from . import purchase_order_line from . import product from . import price_markup_table +from . import purchase_order +from . import res_config_settings diff --git a/bahmni_purchase/models/price_markup_table.py b/bahmni_purchase/models/price_markup_table.py index da450e7..ed725f9 100644 --- a/bahmni_purchase/models/price_markup_table.py +++ b/bahmni_purchase/models/price_markup_table.py @@ -1,6 +1,5 @@ -from odoo import fields, models from odoo import api, fields, models, _, Command -from odoo.exceptions import UserError, ValidationError, AccessError, RedirectWarning +from odoo.exceptions import ValidationError class PriceMarkupTable(models.Model): @@ -11,22 +10,33 @@ class PriceMarkupTable(models.Model): markup_percentage = fields.Float(string="Markup Percentage", default=1) @api.constrains('lower_price', 'higher_price','markup_percentage') - def _check_fields_values(self): + def _check_fields_values(self): if self.lower_price < 0 or self.higher_price < 0 or self.markup_percentage < 0: - raise ValidationError('Negative values are not allowed for Minimum Cost, Maximum Cost and Markup Percentage.') - + raise ValidationError('Negative values are not allowed for Minimum Cost, Maximum Cost and Markup Percentage.') + if self.markup_percentage > 100: - raise ValidationError('Markup percentage should not exceed 100%. Please enter a valid markup percentage.') - + raise ValidationError('Markup percentage should not exceed 100%. Please enter a valid markup percentage.') + if self.lower_price >= self.higher_price: raise ValidationError('Minimum cost should not be greater than maximum cost.') - # Add any other conditions you need to check - for data in self.env['price.markup.table'].search([]): - if data.lower_price < self.lower_price < data.higher_price and data.id != self.id: + # Add any other conditions you need to check + for data in self.env['price.markup.table'].search([]): + if data.lower_price < self.lower_price < data.higher_price and data.id != self.id: raise ValidationError('Your minimum cost is available within the range of minimum cost and maximum cost of previous records.') - + if data.lower_price < self.higher_price < data.higher_price and data.id != self.id: raise ValidationError('Your maximum cost is available within the range of minimum cost and maximum cost of previous records.') - - if self.lower_price < data.lower_price < self.higher_price and data.id != self.id: + + if self.lower_price < data.lower_price < self.higher_price and data.id != self.id: raise ValidationError('Your minimum cost is available within the range of minimum cost and maximum cost of previous records.') + + def calculate_price_with_markup(self, price): + markup_table_line = self.search([('lower_price', '<', price), + '|', ('higher_price', '>=', price), + ('higher_price', '=', 0)], limit=1) + if markup_table_line: + return price + (price * markup_table_line.markup_percentage / 100) + else: + return price + + diff --git a/bahmni_purchase/models/purchase_order.py b/bahmni_purchase/models/purchase_order.py new file mode 100644 index 0000000..12e04b2 --- /dev/null +++ b/bahmni_purchase/models/purchase_order.py @@ -0,0 +1,45 @@ +from odoo import models, fields, api + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + def button_confirm(self): + res = super(PurchaseOrder, self).button_confirm() + if bool(self.env['ir.config_parameter'].sudo().get_param( + 'bahmni_purchase.update_product_prices_on_po_confirm')): + for order in self: + for line in order.order_line: + price_unit, sale_price, mrp = self._calculate_prices(line) + self._update_price_for_supplier(line.product_id.product_tmpl_id.id, price_unit, mrp) + self._update_price_for_product(line.product_id, price_unit, sale_price, mrp) + return res + + def _calculate_prices(self, purchase_order_line): + price_unit = purchase_order_line.price_unit + mrp = purchase_order_line.mrp + tax = purchase_order_line.price_tax / purchase_order_line.product_qty if purchase_order_line.product_qty > 0 else purchase_order_line.price_tax + # Compute the price_unit,mrp for the template's UoM, because the supplier's UoM is related to that UoM. + if purchase_order_line.product_id.product_tmpl_id.uom_po_id != purchase_order_line.product_uom: + default_uom = purchase_order_line.product_id.product_tmpl_id.uom_po_id + price_unit = purchase_order_line.product_uom._compute_price(price_unit, default_uom) + mrp = purchase_order_line.product_uom._compute_price(mrp, default_uom) + tax = purchase_order_line.product_uom._compute_price(tax, default_uom) + total_cost = price_unit + tax + sale_price = self.env['price.markup.table'].calculate_price_with_markup(total_cost) + # Set sale_price as none if no markup value is added + if sale_price == total_cost: + sale_price = None + return price_unit, sale_price, mrp + + def _update_price_for_product(self, product_id, price_unit, sale_price, mrp): + if sale_price: + product_id.write({'standard_price': price_unit, 'list_price': sale_price, 'mrp': mrp}) + else: + product_id.write({'standard_price': price_unit, 'mrp': mrp}) + + def _update_price_for_supplier(self, product_tmpl_id, price_unit, mrp): + seller_info = self.env['product.supplierinfo'].search([('partner_id', '=', self.partner_id.id), + ('product_tmpl_id', '=', product_tmpl_id)]) + seller_info.mrp = mrp + seller_info.price = price_unit diff --git a/bahmni_purchase/models/purchase_order_line.py b/bahmni_purchase/models/purchase_order_line.py index 3400772..68f9101 100644 --- a/bahmni_purchase/models/purchase_order_line.py +++ b/bahmni_purchase/models/purchase_order_line.py @@ -22,7 +22,7 @@ def onchange_product_id(self): @api.onchange('product_qty', 'product_uom') def _onchange_quantity(self): '''Method to get mrp for product from vendor configuration in product master''' - if not self.product_id: + if not self.product_id or not self.product_uom: return seller = self.product_id._select_seller( @@ -33,19 +33,16 @@ def _onchange_quantity(self): if seller or not self.date_planned: self.date_planned = self._get_date_planned(seller).strftime(DTF) - if not seller: - self.mrp = self.product_id.mrp - return + if not seller or (seller and seller.mrp == 0): + mrp = self.product_id.product_tmpl_id.mrp + if self.product_id.product_tmpl_id.uom_po_id != self.product_uom: + default_uom = self.product_id.product_tmpl_id.uom_po_id + mrp = default_uom._compute_price(mrp, self.product_uom) + else: + mrp = seller.mrp + if mrp and self.product_uom and seller.product_uom != self.product_uom: + mrp = seller.product_uom._compute_price(mrp, self.product_uom) + if mrp and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id: + mrp = seller.currency_id.compute(mrp, self.order_id.currency_id) self.manufacturer = seller.manufacturer.id - price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0 - mrp = self.env['account.tax']._fix_tax_included_price_company(seller.mrp, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0 - if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id: - price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id) - if mrp and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id: - mrp = seller.currency_id.compute(mrp, self.order_id.currency_id) - if seller and self.product_uom and seller.product_uom != self.product_uom: - price_unit = seller.product_uom._compute_price(price_unit, self.product_uom) - if mrp and self.product_uom and seller.product_uom != self.product_uom: - mrp = seller.product_uom._compute_price(mrp, self.product_uom) - self.price_unit = price_unit self.mrp = mrp diff --git a/bahmni_purchase/models/res_config_settings.py b/bahmni_purchase/models/res_config_settings.py new file mode 100644 index 0000000..7761167 --- /dev/null +++ b/bahmni_purchase/models/res_config_settings.py @@ -0,0 +1,24 @@ +from odoo import models, fields, api + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + update_product_prices_on_po_confirmation = fields.Boolean( + string="Update price details of the product on Purchase Order Confirmation", + config_parameter="bahmni_purchase.update_product_prices_on_po_confirm") + + def set_values(self): + res = super(ResConfigSettings, self).set_values() + self.env['ir.config_parameter'].sudo().set_param('bahmni_purchase.update_product_prices_on_po_confirm', + self.update_product_prices_on_po_confirmation) + return res + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + ICPSudo = self.env['ir.config_parameter'].sudo() + res.update( + update_product_prices_on_po_confirmation=ICPSudo.get_param('bahmni_purchase.update_product_prices_on_po_confirm'), + ) + return res diff --git a/bahmni_purchase/views/res_config_inherit.xml b/bahmni_purchase/views/res_config_inherit.xml new file mode 100644 index 0000000..890bd72 --- /dev/null +++ b/bahmni_purchase/views/res_config_inherit.xml @@ -0,0 +1,29 @@ + + + + res.config.settings.view.form.inherit.bahmni.payments + res.config.settings + + + + +
+

Purchase Configurations

+
+
+ +
+
+
+ +
+
+ +
+
+
+
diff --git a/bahmni_stock/models/stock_move_line.py b/bahmni_stock/models/stock_move_line.py index 4f6a351..6486785 100644 --- a/bahmni_stock/models/stock_move_line.py +++ b/bahmni_stock/models/stock_move_line.py @@ -1,109 +1,73 @@ import datetime -from dateutil import tz -from collections import Counter, defaultdict - +from collections import defaultdict from odoo import models, fields, api -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DTF - from odoo.exceptions import UserError, ValidationError, AccessError, RedirectWarning + class StockMoveLine(models.Model): - _inherit = 'stock.move.line' - + _inherit = 'stock.move.line' + sale_price = fields.Float(string="Sale Price") mrp = fields.Float(string="MRP") - cost_price = fields.Float(string="Cost Price") - balance = fields.Float(string="Balance") + cost_price = fields.Float(string="Cost Price") + balance = fields.Float(string="Balance") existing_lot_id = fields.Many2one( 'stock.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) - + @api.onchange('product_id','qty_done') - def _onchange_balance_qty(self): + def _onchange_balance_qty(self): if self.location_id and self.product_id: self.balance = (sum([stock.inventory_quantity_auto_apply for stock in self.env['stock.quant'].search([('location_id', '=', self.location_id.id),('product_id', '=', self.product_id.id)])])) - self.qty_done - + @api.onchange('existing_lot_id') - def _onchange_existing_lot_id(self): - if self.existing_lot_id: - self.lot_name = self.existing_lot_id.name - self.expiration_date = self.existing_lot_id.expiration_date + def _onchange_existing_lot_id(self): + if self.existing_lot_id: + self.lot_name = self.existing_lot_id.name + self.expiration_date = self.existing_lot_id.expiration_date else: - self.lot_name = '' + self.lot_name = '' self.expiration_date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time) - + @api.constrains('mrp') def _check_fields_values(self): for rec in self: - if rec.mrp > 0.00: + if rec.mrp > 0.00: if rec.sale_price > rec.mrp: raise ValidationError('Sales Price should not be greater than MRP Rate.') else: pass - + @api.onchange('cost_price') - def _check_cost_price_values(self): - if self.cost_price > 0.00: - markup_table_line = self.env['price.markup.table'].search([('lower_price', '<', self.cost_price), - '|', ('higher_price', '>=', self.cost_price), - ('higher_price', '=', 0)],limit=1) - if markup_table_line: - self.sale_price = self.cost_price + (self.cost_price * markup_table_line.markup_percentage / 100) - else: - self.sale_price = self.cost_price - else: - pass - + def _check_cost_price_values(self): + if self.cost_price > 0.00: + self.sale_price = self.env['price.markup.table'].calculate_price_with_markup(self.cost_price) + @api.model def default_get(self, fields): res = super(StockMoveLine, self).default_get(fields) - cost_value = 0.00 move_ids = self.env['stock.move'].search([('picking_id', '=', res.get('picking_id')),('product_id', '=', res.get('product_id'))],limit=1) - if move_ids: - cost_value = move_ids.purchase_line_id.price_unit + (move_ids.purchase_line_id.price_tax / move_ids.purchase_line_id.product_qty) - cost_value_per_unit = float(cost_value) * float(move_ids.purchase_line_id.product_uom.factor) - if cost_value_per_unit > 0.00: - markup_table_line = self.env['price.markup.table'].search([('lower_price', '<', cost_value_per_unit), - '|', ('higher_price', '>=', cost_value_per_unit), - ('higher_price', '=', 0)],limit=1) - if markup_table_line: - res.update({'cost_price': cost_value_per_unit,'sale_price': cost_value_per_unit + (cost_value_per_unit * markup_table_line.markup_percentage / 100)}) - else: - res.update({'cost_price': cost_value_per_unit,'sale_price': cost_value_per_unit }) + associated_purchase_line = move_ids.purchase_line_id + if associated_purchase_line: + res.update({'mrp': associated_purchase_line.product_uom._compute_price(associated_purchase_line.mrp, self.product_uom_id)}) + total_cost_value = associated_purchase_line.price_unit + (associated_purchase_line.price_tax / associated_purchase_line.product_qty) + cost_value_per_unit = associated_purchase_line.product_uom._compute_price(total_cost_value, self.product_uom_id) + if cost_value_per_unit > 0.00: + res.update({'cost_price': cost_value_per_unit, + 'sale_price': self.env['price.markup.table'].calculate_price_with_markup(cost_value_per_unit) + }) else: pass return res - - def _create_and_assign_production_lot(self): - """ Creates and assign new production lots for move lines.""" - lot_vals = [] - # It is possible to have multiple time the same lot to create & assign, - # so we handle the case with 2 dictionaries. - key_to_index = {} # key to index of the lot - key_to_mls = defaultdict(lambda: self.env['stock.move.line']) # key to all mls - for ml in self: - ml.product_id.write({'standard_price': ml.cost_price,'list_price': ml.sale_price,'mrp': ml.mrp}) - key = (ml.company_id.id, ml.product_id.id, ml.lot_name, ml.cost_price, ml.sale_price, ml.mrp, ml.expiration_date) - key_to_mls[key] |= ml - if ml.tracking != 'lot' or key not in key_to_index: - key_to_index[key] = len(lot_vals) - lot_vals.append(ml._get_value_production_lot()) - lots = self.env['stock.lot'].create(lot_vals) - for key, mls in key_to_mls.items(): - lot = lots[key_to_index[key]].with_prefetch(lots._ids) # With prefetch to reconstruct the ones broke by accessing by index - mls.write({'lot_id': lot.id}) - + + # This function is overridden to set price details for the lot to help markup feature. + # This will be called on receive products def _get_value_production_lot(self): - self.ensure_one() - - return { - 'company_id': self.company_id.id, - 'name': self.lot_name, - 'product_id': self.product_id.id, + res = super(StockMoveLine,self)._get_value_production_lot() + res.update({ 'cost_price': self.cost_price, 'sale_price': self.sale_price, - 'mrp': self.mrp, - 'expiration_date': self.expiration_date - } - + 'mrp': self.mrp + }) + return res diff --git a/bahmni_stock/models/stock_picking.py b/bahmni_stock/models/stock_picking.py index 0f58323..e455a45 100644 --- a/bahmni_stock/models/stock_picking.py +++ b/bahmni_stock/models/stock_picking.py @@ -9,73 +9,39 @@ class StockPicking(models.Model): _inherit = 'stock.picking' - + def button_validate(self): - # Clean-up the context key at validation to avoid forcing the creation of immediate - # transfers. - ctx = dict(self.env.context) - ctx.pop('default_immediate_transfer', None) - self = self.with_context(ctx) - ### Internal Transfer Batch based code write - if self.picking_type_id: - stock_picking_type = self.env['stock.picking.type'].search([('id','=', self.picking_type_id.id)]) - if stock_picking_type.code == 'internal': - for line in self.move_line_ids: - if line.product_id.tracking != 'none': - stock_quant_lot = self.env['stock.quant'].search([ - ('product_id','=', line.product_id.id),('location_id', '=', line.location_id.id), - ('lot_id', '=', line.lot_id.id),('quantity', '>' , 0)]) - if stock_quant_lot.quantity < line.qty_done: - raise UserError("Insufficient batch(%s) quantity for %s and available quantity is %s"\ - %(line.lot_id.name, line.product_id.name, stock_quant_lot.quantity)) - # Sanity checks. - if not self.env.context.get('skip_sanity_check', False): - self._sanity_check() - - self.message_subscribe([self.env.user.partner_id.id]) - - # Run the pre-validation wizards. Processing a pre-validation wizard should work on the - # moves and/or the context and never call `_action_done`. - if not self.env.context.get('button_validate_picking_ids'): - self = self.with_context(button_validate_picking_ids=self.ids) - res = self._pre_action_done_hook() - if res is not True: - return res - - # Call `_action_done`. - pickings_not_to_backorder = self.filtered(lambda p: p.picking_type_id.create_backorder == 'never') - if self.env.context.get('picking_ids_not_to_backorder'): - pickings_not_to_backorder |= self.browse(self.env.context['picking_ids_not_to_backorder']).filtered( - lambda p: p.picking_type_id.create_backorder != 'always' - ) - pickings_to_backorder = self - pickings_not_to_backorder - pickings_not_to_backorder.with_context(cancel_backorder=True)._action_done() - pickings_to_backorder.with_context(cancel_backorder=False)._action_done() - - if self.user_has_groups('stock.group_reception_report'): - pickings_show_report = self.filtered(lambda p: p.picking_type_id.auto_show_reception_report) - lines = pickings_show_report.move_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel' and m.quantity_done and not m.move_dest_ids) - if lines: - # don't show reception report if all already assigned/nothing to assign - wh_location_ids = self.env['stock.location']._search([('id', 'child_of', pickings_show_report.picking_type_id.warehouse_id.view_location_id.ids), ('usage', '!=', 'supplier')]) - if self.env['stock.move'].search([ - ('state', 'in', ['confirmed', 'partially_available', 'waiting', 'assigned']), - ('product_qty', '>', 0), - ('location_id', 'in', wh_location_ids), - ('move_orig_ids', '=', False), - ('picking_id', 'not in', pickings_show_report.ids), - ('product_id', 'in', lines.product_id.ids)], limit=1): - action = pickings_show_report.action_view_reception_report() - action['context'] = {'default_picking_ids': pickings_show_report.ids} - return action - return True - + self.validate_batch_quantity_for_internal_transfer() + res = super(StockPicking, self).button_validate() + self.update_price_details_for_lots() + return res + + def validate_batch_quantity_for_internal_transfer(self): + if self.picking_type_id and self.picking_type_id.code == 'internal': + for line in self.move_line_ids: + if line.product_id.tracking != 'none': + stock_quant_lot = self.env['stock.quant'].search([ + ('product_id','=', line.product_id.id),('location_id', '=', line.location_id.id), + ('lot_id', '=', line.lot_id.id),('quantity', '>' , 0)]) + if stock_quant_lot.quantity < line.qty_done: + raise UserError("Insufficient batch(%s) quantity for %s and available quantity is %s" \ + %(line.lot_id.name, line.product_id.name, stock_quant_lot.quantity)) + + def update_price_details_for_lots(self): + if self.picking_type_id and self.picking_type_id.code == 'incoming' and self.picking_type_id.name == 'Receipts': + for line in self.move_line_ids: + if line.product_id.tracking != 'none': + line.lot_id.cost_price = line.cost_price + line.lot_id.sale_price = line.sale_price + line.lot_id.mrp = line.mrp + line.lot_id.expiration_date = line.expiration_date + # this method is overridden to update cost_price, sale_price and mrp while lot is getting created def _create_lots_for_picking(self): Lot = self.env['stock.production.lot'] for pack_op_lot in self.mapped('pack_operation_ids').mapped('pack_lot_ids'): if not pack_op_lot.lot_id: - lot = Lot.create({'name': pack_op_lot.lot_name, + lot = Lot.create({'name': pack_op_lot.lot_name, 'product_id': pack_op_lot.operation_id.product_id.id, 'life_date': pack_op_lot.expiry_date, 'cost_price': pack_op_lot.cost_price, @@ -83,12 +49,12 @@ def _create_lots_for_picking(self): 'mrp': pack_op_lot.mrp}) pack_op_lot.write({'lot_id': lot.id}) self.mapped('pack_operation_ids').mapped('pack_lot_ids').filtered(lambda op_lot: op_lot.qty == 0.0).unlink() - + def do_prepare_partial(self): PackOperation = self.env['stock.pack.operation'] # get list of existing operations and delete them - existing_packages = PackOperation.search([('picking_id', 'in', self.ids)]) + existing_packages = PackOperation.search([('picking_id', 'in', self.ids)]) if existing_packages: existing_packages.unlink() for picking in self: @@ -123,7 +89,7 @@ def do_prepare_partial(self): pack.mapped('linked_move_operation_ids').mapped('move_id').filtered(lambda r: r.state != 'cancel').mapped('ordered_qty') ) self.write({'recompute_pack_op': False}) - + # this method is overridden to update available qty for lot, and expiration date, when automatically reserved def _prepare_pack_ops(self, quants, forced_qties): """ Prepare pack_operations, returns a list of dict to give at create """ @@ -210,7 +176,7 @@ def _prepare_pack_ops(self, quants, forced_qties): values = product_id_to_vals.pop(move.product_id.id, []) pack_operation_values += values return pack_operation_values - + @api.model def create(self,vals): picking_obj = super(StockPicking,self).create(vals) diff --git a/bahmni_stock/views/stock_pick_lot_view.xml b/bahmni_stock/views/stock_pick_lot_view.xml index f341c89..8fe5668 100644 --- a/bahmni_stock/views/stock_pick_lot_view.xml +++ b/bahmni_stock/views/stock_pick_lot_view.xml @@ -1,28 +1,39 @@ - + stock.move.line.operations.tree.inherit stock.move.line - + - - + + + {'readonly': ['|',('existing_lot_id', '!=', False),'&', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True)]} + + 1 + + + { + 'column_invisible': ['|', ('parent.use_expiration_date', '!=', True), ('parent.picking_code', '!=', 'incoming')], + 'readonly': ['|',('picking_type_use_existing_lots', '=', True), ('existing_lot_id', '!=', False)] } + + + - + stock.move.line.detailed.operations.tree.inherit stock.move.line - + @@ -33,5 +44,5 @@ - +