/**
* Axelor Business Solutions
*
* Copyright (C) 2016 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.axelor.apps.supplychain.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.axelor.apps.base.db.Partner;
import com.axelor.apps.base.db.Product;
import com.axelor.apps.base.db.SupplierCatalog;
import com.axelor.apps.base.db.Unit;
import com.axelor.apps.base.db.repo.ProductRepository;
import com.axelor.apps.base.service.UnitConversionService;
import com.axelor.apps.base.service.administration.GeneralService;
import com.axelor.apps.purchase.db.IPurchaseOrder;
import com.axelor.apps.purchase.db.PurchaseOrder;
import com.axelor.apps.purchase.db.PurchaseOrderLine;
import com.axelor.apps.purchase.db.repo.PurchaseOrderLineRepository;
import com.axelor.apps.sale.db.ISaleOrder;
import com.axelor.apps.sale.db.SaleOrder;
import com.axelor.apps.sale.db.SaleOrderLine;
import com.axelor.apps.sale.db.repo.SaleOrderLineRepository;
import com.axelor.apps.sale.db.repo.SaleOrderRepository;
import com.axelor.apps.stock.db.Location;
import com.axelor.apps.stock.db.LocationLine;
import com.axelor.apps.stock.db.MinStockRules;
import com.axelor.apps.stock.db.repo.LocationLineRepository;
import com.axelor.apps.stock.db.repo.LocationRepository;
import com.axelor.apps.stock.db.repo.MinStockRulesRepository;
import com.axelor.apps.stock.service.MinStockRulesService;
import com.axelor.apps.supplychain.db.Mrp;
import com.axelor.apps.supplychain.db.MrpFamily;
import com.axelor.apps.supplychain.db.MrpForecast;
import com.axelor.apps.supplychain.db.MrpLine;
import com.axelor.apps.supplychain.db.MrpLineOrigin;
import com.axelor.apps.supplychain.db.MrpLineType;
import com.axelor.apps.supplychain.db.repo.MrpForecastRepository;
import com.axelor.apps.supplychain.db.repo.MrpLineRepository;
import com.axelor.apps.supplychain.db.repo.MrpLineTypeRepository;
import com.axelor.apps.supplychain.db.repo.MrpRepository;
import com.axelor.apps.supplychain.exception.IExceptionMessage;
import com.axelor.db.Model;
import com.axelor.exception.AxelorException;
import com.axelor.exception.db.IException;
import com.axelor.i18n.I18n;
import com.axelor.inject.Beans;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
public class MrpServiceImpl implements MrpService {
private final Logger log = LoggerFactory.getLogger( getClass() );
protected MrpRepository mrpRepository;
protected LocationRepository locationRepository;
protected ProductRepository productRepository;
protected LocationLineRepository locationLineRepository;
protected MrpLineTypeRepository mrpLineTypeRepository;
protected PurchaseOrderLineRepository purchaseOrderLineRepository;
protected SaleOrderLineRepository saleOrderLineRepository;
protected MrpLineRepository mrpLineRepository;
protected MinStockRulesService minStockRulesService;
protected MrpLineService mrpLineService;
protected MrpForecastRepository mrpForecastRepository;
protected LocalDate today;
protected List<Location> locationList = Lists.newArrayList();
protected Map<Product,Integer> productMap = Maps.newHashMap();
protected Mrp mrp;
@Inject
public MrpServiceImpl(GeneralService generalService, MrpRepository mrpRepository, LocationRepository locationRepository,
ProductRepository productRepository, LocationLineRepository locationLineRepository, MrpLineTypeRepository mrpLineTypeRepository,
PurchaseOrderLineRepository purchaseOrderLineRepository, SaleOrderLineRepository saleOrderLineRepository, MrpLineRepository mrpLineRepository,
MinStockRulesService minStockRulesService, MrpLineService mrpLineService, MrpForecastRepository mrpForecastRepository) {
this.mrpRepository = mrpRepository;
this.locationRepository = locationRepository;
this.productRepository = productRepository;
this.locationLineRepository = locationLineRepository;
this.mrpLineTypeRepository = mrpLineTypeRepository;
this.purchaseOrderLineRepository = purchaseOrderLineRepository;
this.saleOrderLineRepository = saleOrderLineRepository;
this.mrpLineRepository = mrpLineRepository;
this.minStockRulesService = minStockRulesService;
this.mrpLineService = mrpLineService;
this.mrpForecastRepository = mrpForecastRepository;
this.today = generalService.getTodayDate();
}
public void runCalculation(Mrp mrp) throws AxelorException {
this.startMrp(mrpRepository.find(mrp.getId()));
this.completeMrp(mrpRepository.find(mrp.getId()));
this.doCalulation(mrpRepository.find(mrp.getId()));
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
protected void startMrp(Mrp mrp) {
log.debug("Start MRP");
mrp.setStatusSelect(MrpRepository.STATUS_CALCULATION_STARTED);
//TODO check that the different types used for purchase/manufOrder proposal are in stock type
//TODO check that all types exist + override the method on production module
mrp.clearMrpLineList();
mrpRepository.save(mrp);
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public void reset(Mrp mrp) {
mrp.setStatusSelect(MrpRepository.STATUS_DRAFT);
mrp.clearMrpLineList();
mrpRepository.save(mrp);
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
protected void completeMrp(Mrp mrp) throws AxelorException {
log.debug("Complete MRP");
// Initialize
this.mrp = mrp;
this.locationList = this.getAllLocationAndSubLocation(mrp.getLocation());
this.assignProductAndLevel(this.getProductList());
// Get the stock for each product on each location
this.createAvailableStockMrpLines();
this.createPurchaseMrpLines();
this.createSaleOrderMrpLines();
this.createSaleForecastMrpLines();
mrpRepository.save(mrp);
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
protected void doCalulation(Mrp mrp) throws AxelorException {
log.debug("Do calculation");
mrpRepository.save(mrp);
this.checkInsufficientCumulativeQty();
// this.consolidateMrp(mrp);
mrp.setStatusSelect(MrpRepository.STATUS_CALCULATION_ENDED);
}
protected void checkInsufficientCumulativeQty() throws AxelorException {
for(int level = 0; level <= this.getMaxLevel(); level++) {
for(Product product : this.getProductList(level)) {
this.checkInsufficientCumulativeQty(product);
}
}
}
/**
* Get the list of product for a level
* @param level
* @return
*/
protected List<Product> getProductList(int level) {
List<Product> productList = Lists.newArrayList();
for(Product product : this.productMap.keySet()) {
if(this.productMap.get(product) == level) {
productList.add(product);
}
}
return productList;
}
protected int getMaxLevel() {
int maxLevel = 0;
for(int level : this.productMap.values()) {
if(level > maxLevel) { maxLevel = level; }
}
return maxLevel;
}
protected void checkInsufficientCumulativeQty(Product product) throws AxelorException {
boolean doASecondPass = false;
this.computeCumulativeQty(product);
List<MrpLine> mrpLineList = mrpLineRepository.all().filter("self.mrp = ?1 AND self.product = ?2", mrp, product).order("maturityDate").order("mrpLineType.typeSelect").order("mrpLineType.sequence").order("id").fetch();
for(MrpLine mrpLine : mrpLineList) {
BigDecimal cumulativeQty = mrpLine.getCumulativeQty();
MrpLineType mrpLineType = mrpLine.getMrpLineType();
boolean isProposalElement = this.isProposalElement(mrpLineType);
BigDecimal minQty = mrpLine.getMinQty();
if(mrpLine.getMrpLineType().getElementSelect() != MrpLineTypeRepository.ELEMENT_AVAILABLE_STOCK
&& (!isProposalElement || mrpLineType.getTypeSelect() == MrpLineTypeRepository.TYPE_OUT)
&& cumulativeQty.compareTo(mrpLine.getMinQty()) == -1) {
log.debug("Cumulative qty ({} < {}) is insufficient for product ({}) at the maturity date ({})", cumulativeQty, minQty, product.getFullName(), mrpLine.getMaturityDate());
BigDecimal reorderQty = minQty.subtract(cumulativeQty);
MinStockRules minStockRules = minStockRulesService.getMinStockRules(product, mrpLine.getLocation(), MinStockRulesRepository.TYPE_FUTURE);
if(minStockRules != null) { reorderQty = reorderQty.max(minStockRules.getReOrderQty()); }
MrpLineType mrpLineTypeProposal = this.getMrpLineTypeForProposal(minStockRules);
this.createProposalMrpLine(product, mrpLineTypeProposal, reorderQty, mrpLine.getLocation(), mrpLine.getMaturityDate(), mrpLine.getMrpLineOriginList(), mrpLine.getRelatedToSelectName());
doASecondPass = true;
break;
}
}
if(doASecondPass) {
mrpRepository.save(mrp);
this.checkInsufficientCumulativeQty(product);
}
}
public MrpLine getPreviousProposalMrpLine(Product product, MrpLineType mrpLineType, Location location, LocalDate maturityDate) {
LocalDate startPeriodDate = maturityDate;
MrpFamily mrpFamily = product.getMrpFamily();
if(mrpFamily != null) {
if(mrpFamily.getDayNb() == 0) { return null; }
startPeriodDate = maturityDate.minusDays(mrpFamily.getDayNb());
}
return mrpLineRepository.all().filter("self.mrp = ?1 AND self.product = ?2 AND self.mrpLineType = ?3 AND self.location = ?4 AND self.maturityDate > ?5 AND self.maturityDate <= ?6",
mrp, product, mrpLineType, location, startPeriodDate, maturityDate).fetchOne();
}
protected void createProposalMrpLine(Product product, MrpLineType mrpLineType, BigDecimal reorderQty, Location location, LocalDate maturityDate, List<MrpLineOrigin> mrpLineOriginList, String relatedToSelectName) throws AxelorException {
if(mrpLineType.getElementSelect() == MrpLineTypeRepository.ELEMENT_PURCHASE_PROPOSAL) {
maturityDate = maturityDate.minusDays(product.getSupplierDeliveryTime());
reorderQty = reorderQty.max(this.getSupplierCatalogMinQty(product));
}
MrpLine mrpLine = this.getPreviousProposalMrpLine(product, mrpLineType, location, maturityDate);
if(mrpLine != null) {
mrpLine.setQty(mrpLine.getQty().add(reorderQty));
mrpLine.setRelatedToSelectName(null);
}
else {
mrpLine = mrpLineRepository.save(this.createMrpLine(product, mrpLineType, reorderQty, maturityDate, BigDecimal.ZERO, location));
mrp.addMrpLineListItem(mrpLine);
mrpLine.setRelatedToSelectName(relatedToSelectName);
}
this.copyMrpLineOrigins(mrpLine, mrpLineOriginList);
}
protected BigDecimal getSupplierCatalogMinQty(Product product) {
Partner supplierPartner = product.getDefaultSupplierPartner();
if(supplierPartner != null) {
for(SupplierCatalog supplierCatalog : product.getSupplierCatalogList()) {
if(supplierCatalog.getSupplierPartner().equals(supplierPartner)) {
return supplierCatalog.getMinQty();
}
}
}
return BigDecimal.ZERO;
}
protected MrpLineType getMrpLineTypeForProposal(MinStockRules minStockRules) throws AxelorException {
return this.getMrpLineType(MrpLineTypeRepository.ELEMENT_PURCHASE_PROPOSAL);
}
protected void consolidateMrp() {
List<MrpLine> mrpLineList = mrpLineRepository.all().filter("self.mrp = ?1", mrp).order("self.product.code").order("maturityDate").order("mrpLineType.typeSelect").order("mrpLineType.sequence").order("id").fetch();
Map<List<Object>, MrpLine> map = Maps.newHashMap();
MrpLine consolidateMrpLine = null;
List<Object> keys = new ArrayList<Object>();
for (MrpLine mrpLine : mrpLineList){
MrpLineType mrpLineType = mrpLine.getMrpLineType();
keys.clear();
keys.add(mrpLineType);
keys.add(mrpLine.getProduct());
keys.add(mrpLine.getMaturityDate());
keys.add(mrpLine.getLocation());
if (map.containsKey(keys)) {
consolidateMrpLine = map.get(keys);
consolidateMrpLine.setQty(consolidateMrpLine.getQty().add(mrpLine.getQty()));
consolidateMrpLine.setCumulativeQty(consolidateMrpLine.getCumulativeQty().add(mrpLine.getCumulativeQty()));
}
else {
map.put(keys, mrpLine);
}
}
mrp.getMrpLineList().clear();
mrp.getMrpLineList().addAll(map.values());
}
protected boolean isProposalElement(MrpLineType mrpLineType) {
if(mrpLineType.getElementSelect() == MrpLineTypeRepository.ELEMENT_PURCHASE_PROPOSAL) {
return true;
}
return false;
}
protected void computeCumulativeQty() {
for(Product product : this.productMap.keySet()) {
this.computeCumulativeQty(product);
}
}
protected void computeCumulativeQty(Product product) {
List<MrpLine> mrpLineList = mrpLineRepository.all().filter("self.mrp = ?1 AND self.product = ?2", mrp, product).order("maturityDate").order("mrpLineType.typeSelect").order("mrpLineType.sequence").order("id").fetch();
BigDecimal previousCumulativeQty = BigDecimal.ZERO;
for(MrpLine mrpLine : mrpLineList) {
if(mrpLine.getMrpLineType().getElementSelect() == MrpLineTypeRepository.ELEMENT_AVAILABLE_STOCK) {
mrpLine.setCumulativeQty(mrpLine.getQty());
}
else {
mrpLine.setCumulativeQty(previousCumulativeQty.add(mrpLine.getQty()));
}
previousCumulativeQty = mrpLine.getCumulativeQty();
log.debug("Cumulative qty is ({}) for product ({}) and move ({}) at the maturity date ({})",
previousCumulativeQty, mrpLine.getProduct().getFullName(), mrpLine.getMrpLineType().getName(), mrpLine.getMaturityDate());
}
}
// achat ferme
protected void createPurchaseMrpLines() throws AxelorException {
MrpLineType purchaseProposalMrpLineType = this.getMrpLineType(MrpLineTypeRepository.ELEMENT_PURCHASE_ORDER);
// TODO : Manage the case where order is partially delivered
List<PurchaseOrderLine> purchaseOrderLineList = purchaseOrderLineRepository.all()
.filter("self.product in (?1) AND self.purchaseOrder.location in (?2) AND self.purchaseOrder.receiptState = ?3 "
+ "AND self.purchaseOrder.statusSelect = ?4",
this.productMap.keySet(), this.locationList, IPurchaseOrder.STATE_NOT_RECEIVED, IPurchaseOrder.STATUS_VALIDATED).fetch();
for(PurchaseOrderLine purchaseOrderLine : purchaseOrderLineList) {
PurchaseOrder purchaseOrder = purchaseOrderLine.getPurchaseOrder();
LocalDate maturityDate = purchaseOrderLine.getEstimatedDelivDate();
if(maturityDate == null) { maturityDate = purchaseOrder.getDeliveryDate(); }
if(maturityDate == null) { maturityDate = purchaseOrder.getOrderDate(); }
if(this.isBeforeEndDate(maturityDate)) {
Unit unit = purchaseOrderLine.getProduct().getUnit();
BigDecimal qty = purchaseOrderLine.getQty();
if(!unit.equals(purchaseOrderLine.getUnit())){
qty = Beans.get(UnitConversionService.class).convertWithProduct(purchaseOrderLine.getUnit(), unit, qty, purchaseOrderLine.getProduct());
}
mrp.addMrpLineListItem(this.createMrpLine(purchaseOrderLine.getProduct(), purchaseProposalMrpLineType, qty, maturityDate, BigDecimal.ZERO, purchaseOrder.getLocation(), purchaseOrderLine));
}
}
}
// Vente ferme
protected void createSaleOrderMrpLines() throws AxelorException {
MrpLineType saleForecastMrpLineType = this.getMrpLineType(MrpLineTypeRepository.ELEMENT_SALE_ORDER);
// TODO : Manage the case where order is partially delivered
List<SaleOrderLine> saleOrderLineList = new ArrayList<>();
if(mrp.getSaleOrderLineSet().isEmpty()) {
saleOrderLineList.addAll(saleOrderLineRepository.all()
.filter("self.product in (?1) AND self.saleOrder.location in (?2) AND self.saleOrder.deliveryState = ?3 "
+ "AND self.saleOrder.statusSelect = ?4",
this.productMap.keySet(), this.locationList, SaleOrderRepository.STATE_NOT_DELIVERED, ISaleOrder.STATUS_ORDER_CONFIRMED).fetch());
}
else {
saleOrderLineList.addAll(mrp.getSaleOrderLineSet());
}
for(SaleOrderLine saleOrderLine : saleOrderLineList) {
SaleOrder saleOrder = saleOrderLine.getSaleOrder();
LocalDate maturityDate = saleOrderLine.getEstimatedDelivDate();
if(maturityDate == null) { maturityDate = saleOrder.getDeliveryDate(); }
if(maturityDate == null) { maturityDate = saleOrder.getOrderDate(); }
if(maturityDate == null) { maturityDate = saleOrder.getCreationDate(); }
if(this.isBeforeEndDate(maturityDate)) {
Unit unit = saleOrderLine.getProduct().getUnit();
BigDecimal qty = saleOrderLine.getQty();
if(!unit.equals(saleOrderLine.getUnit())){
qty = Beans.get(UnitConversionService.class).convertWithProduct(saleOrderLine.getUnit(), unit, qty, saleOrderLine.getProduct());
}
mrp.addMrpLineListItem(this.createMrpLine(saleOrderLine.getProduct(), saleForecastMrpLineType, qty, maturityDate, BigDecimal.ZERO, saleOrder.getLocation(), saleOrderLine));
}
}
}
protected void createSaleForecastMrpLines() throws AxelorException {
MrpLineType saleForecastMrpLineType = this.getMrpLineType(MrpLineTypeRepository.ELEMENT_SALE_FORECAST);
List<MrpForecast> mrpForecastList = new ArrayList<>();
if(mrp.getMrpForecastSet().isEmpty()) {
mrpForecastList.addAll(mrpForecastRepository.all()
.filter("self.product in (?1) AND self.location in (?2) AND self.forecastDate >= ?3",
this.productMap.keySet(), this.locationList, today, today).fetch());
}
else {
mrpForecastList.addAll(mrp.getMrpForecastSet());
}
for(MrpForecast mrpForecast : mrpForecastList) {
LocalDate maturityDate = mrpForecast.getForecastDate();
if(this.isBeforeEndDate(maturityDate)) {
Unit unit = mrpForecast.getProduct().getUnit();
BigDecimal qty = mrpForecast.getQty();
if(!unit.equals(mrpForecast.getUnit())){
qty = Beans.get(UnitConversionService.class).convertWithProduct(mrpForecast.getUnit(), unit, qty, mrpForecast.getProduct());
}
mrp.addMrpLineListItem(
this.createMrpLine(mrpForecast.getProduct(), saleForecastMrpLineType, qty, maturityDate, BigDecimal.ZERO, mrpForecast.getLocation(), mrpForecast));
}
}
}
public boolean isBeforeEndDate(LocalDate maturityDate) {
if(maturityDate != null && !maturityDate.isBefore(today) && (mrp.getEndDate() == null || !maturityDate.isAfter(mrp.getEndDate()))) {
return true;
}
return false;
}
protected void createAvailableStockMrpLines() throws AxelorException {
MrpLineType availableStockMrpLineType = this.getMrpLineType(MrpLineTypeRepository.ELEMENT_AVAILABLE_STOCK);
for(Product product : this.productMap.keySet()) {
for(Location location : this.locationList) {
mrp.addMrpLineListItem(this.createAvailableStockMrpLine(product, location, availableStockMrpLineType));
}
}
}
protected MrpLine createAvailableStockMrpLine(Product product, Location location, MrpLineType availableStockMrpLineType) {
BigDecimal qty = BigDecimal.ZERO;
LocationLine locationLine = this.getLocationLine(product, location);
if(locationLine != null) {
qty = locationLine.getCurrentQty();
}
return this.createMrpLine(product, availableStockMrpLineType, qty, today, qty, location);
}
protected MrpLineType getMrpLineType(int elementSelect) throws AxelorException {
MrpLineType mrpLineType = mrpLineTypeRepository.all().filter("self.elementSelect = ?1", elementSelect).fetchOne();
if(mrpLineType != null) {
return mrpLineType;
}
throw new AxelorException(
String.format(I18n.get(IExceptionMessage.MRP_MISSING_MRP_LINE_TYPE), elementSelect),
IException.CONFIGURATION_ERROR);
//TODO get the right label in fact of integer value
}
protected LocationLine getLocationLine(Product product, Location location) {
return locationLineRepository.all().filter("self.location = ?1 AND self.product = ?2", location, product).fetchOne();
}
protected Set<Product> getProductList() throws AxelorException {
Set<Product> productSet = Sets.newHashSet();
if(!mrp.getProductSet().isEmpty()) {
productSet.addAll(mrp.getProductSet());
}
if(!mrp.getProductCategorySet().isEmpty()) {
productSet.addAll(productRepository.all().filter("self.productCategory in (?1) AND self.productTypeSelect = ?2 AND self.excludeFromMrp = false",
mrp.getProductCategorySet(), ProductRepository.PRODUCT_TYPE_STORABLE).fetch());
}
if(!mrp.getProductFamilySet().isEmpty()) {
productSet.addAll(productRepository.all().filter("self.productFamily in (?1) AND self.productTypeSelect = ?2 AND self.excludeFromMrp = false",
mrp.getProductFamilySet(), ProductRepository.PRODUCT_TYPE_STORABLE).fetch());
}
for(SaleOrderLine saleOrderLine : mrp.getSaleOrderLineSet()) {
productSet.add(saleOrderLine.getProduct());
}
for(MrpForecast mrpForecast : mrp.getMrpForecastSet()) {
productSet.add(mrpForecast.getProduct());
}
if(productSet.isEmpty()) {
throw new AxelorException(
String.format(I18n.get(IExceptionMessage.MRP_NO_PRODUCT)),
IException.CONFIGURATION_ERROR);
}
return productSet;
}
public boolean isMrpProduct(Product product) {
if(product != null && !product.getExcludeFromMrp() && product.getProductTypeSelect().equals(ProductRepository.PRODUCT_TYPE_STORABLE)) {
return true;
}
return false;
}
protected void assignProductAndLevel(Set<Product> productList) {
for(Product product : productList) {
this.assignProductAndLevel(product);
}
}
protected void assignProductAndLevel(Product product) {
log.debug("Add of the product : {}", product.getFullName());
this.productMap.put(product, 0);
}
protected MrpLine createMrpLine(Product product, MrpLineType mrpLineType, BigDecimal qty, LocalDate maturityDate, BigDecimal cumulativeQty, Location location, Model... models) {
return mrpLineService.createMrpLine(product, this.productMap.get(product), mrpLineType, qty, maturityDate, cumulativeQty, location, models);
}
protected void copyMrpLineOrigins(MrpLine mrpLine, List<MrpLineOrigin> mrpLineOriginList) {
if(mrpLineOriginList != null) {
for(MrpLineOrigin mrpLineOrigin : mrpLineOriginList) {
mrpLine.addMrpLineOriginListItem(mrpLineService.copyMrpLineOrigin(mrpLineOrigin));
}
}
}
protected List<Location> getAllLocationAndSubLocation(Location location) {
List<Location> subLocationList = locationRepository.all().filter("self.parent = ?1", location).fetch();
for(Location subLocation : subLocationList) {
subLocationList.addAll(this.getAllLocationAndSubLocation(subLocation));
}
subLocationList.add(location);
return subLocationList;
}
@Transactional(rollbackOn = {AxelorException.class, Exception.class})
public void generateProposals(Mrp mrp) throws AxelorException {
for(MrpLine mrpLine : mrp.getMrpLineList()) {
mrpLineService.generateProposal(mrpLine);
}
}
}