[Devis] MĂ©thode de chiffrage pour entreprise de service / installateur

Bonjour Ă  tous,
Suite au sujet suivant : Conseil process vente-production

J’ai continuĂ© d’avancĂ©, mais je pense que ça mĂ©rite un topic dĂ©diĂ©.

Si j’ai bien compris, le taux de valorisation est la base de calcul pour dĂ©terminer la marge que l’on peut faire sur un devis. Mais pour un devis on n’a pas forcĂ©ment le stock. Donc la valorisation ne marche pas.

Sur excel, voici comment je fais:

Sur dokos, je souhaiterai l’équivalent avec :

  • en B : le prix de l’article Ă  l’achat (=prix achat article de la liste achat standard * remise fournisseur de la rĂšgle de prix sur l’achat)
  • C et D je les ai dĂ©jĂ 
  • afficher E (qui est le rĂ©sultat de B*C)
  • F, G et H sont identique Ă  B,C,D,E mais pour la main d’oeuvre (il y aura des lignes 100% main d’oeuvre et 100% matĂ©riels en utilisant les ensembles)

Ca me permet d’avoir le prix de revient chantier = somme du prix de revient matĂ©riel + prix de revient main d’oeuvre .
Le %MO est un plus mais facilement obtenable.

Cela me permettra ensuite d’obtenir le prix de vente de chaque article (que je peux fixer par la liste de vente standard avec remise ou marge - ça fonctionne dĂ©jĂ  comme ça) et de voir la marge que je me fait dessus.

Etude / transfert devis et frais supplĂ©mentaire doivent ĂȘtre rajoutĂ© par la suite pour me permettre d’intĂ©grĂ© des couts qui doivent ĂȘtre ventilĂ© sur les lignes vendues au dessus.

Comme ça j’obtiens au final mon prix de vente et le rĂ©sultat associĂ©.
Je peux ainsi facilement arrĂȘtĂ© mon prix en toute connaissance de cause.

J’ai vu la mĂ©thode de valorisation de Dokos qui est basĂ© sur la valeur de stock mais quand nous n’avons pas de stock (par exemple nĂ©gociation avec le fournisseur d’une remise sur ses produits, remise qui peut varier), cette mĂ©thode n’est pas applicable.

J’ai trouvĂ© un sujet similaire sur le forum d’ERPNEXT, mais pas forcĂ©ment de solution complĂšte

Comment pourrais je procéder pour mettre cela en place et proposer les adaptations à la communauté.

Merci d’avance

Bonjour @oryxr,

Merci pour ces détails, je comprends mieux votre fonctionnement.

Pour commencer, voici un piste qu’on pourrait explorer pour obtenir le prix d’achat moins les remises d’un fournisseur, pour une ligne d’articles donnĂ©e:

  • On peut imaginer ajouter une section CoĂ»ts dans les lignes de devis:
    Pour le moment, j’ai juste mis 3 champs pour sĂ©lectionner un fournisseur et obtenir le prix de la liste de prix et le prix remisĂ©:

  • Le script client suivant permet d’obtenir des informations sur l’article, en fonction des paramĂštres qui sont passĂ©s:

frappe.ui.form.on('Quotation Item', {
	selected_supplier(frm, cdt, cdn) {
	    const item = locals[cdt][cdn]
		frappe.call({
			method: "erpnext.stock.get_item_details.get_item_details",
			args: {
				doc: frm.doc,
				args: {
					item_code: item.item_code,
					barcode: item.barcode,
					serial_no: item.serial_no,
					batch_no: item.batch_no,
					warehouse: item.warehouse,
					supplier: item.selected_supplier,
					currency: frm.doc.currency,
					update_stock: 0,
					conversion_rate: frm.doc.conversion_rate,
					price_list: "Achat Standard",
					price_list_currency: frm.doc.price_list_currency,
					plc_conversion_rate: frm.doc.plc_conversion_rate,
					company: frm.doc.company,
					ignore_pricing_rule: frm.doc.ignore_pricing_rule,
					doctype: frm.doc.doctype,
					name: frm.doc.name,
					project: item.project || frm.doc.project,
					qty: item.qty || 1,
					net_rate: item.rate,
					stock_qty: item.stock_qty,
					conversion_factor: item.conversion_factor,
					weight_per_unit: item.weight_per_unit,
					uom: item.uom,
					weight_uom: item.weight_uom,
					manufacturer: item.manufacturer,
					stock_uom: item.stock_uom,
					cost_center: item.cost_center,
					tax_category: frm.doc.tax_category,
					item_tax_template: item.item_tax_template,
					child_docname: item.name,
					transaction_type: "buying"
				}
			}
		}).then(r => {
		    console.log(r)
	        frappe.model.set_value(item.doctype, item.name, "supplier_price_list_rate", flt(r.message.price_list_rate))
	        frappe.model.set_value(item.doctype, item.name, "unit_cost", flt(r.message.price_list_rate  - (r.message.price_list_rate * (r.message.discount_percentage || 100) / 100)))
		})
	}
})

Ici j’ai juste codĂ© en dur la liste de prix Ă  utiliser (“Achat Standard”) et le type de transaction (“buying”) pour rĂ©cupĂ©rer les informations d’achat.

Cet appel renvoi les informations suivantes:

{
    "item_code": "ChaudiĂšre",
    "item_name": "ChaudiĂšre",
    "description": "ChaudiĂšre",
    "image": "",
    "warehouse": "Magasins - MM",
    "income_account": "701 - Ventes de produits finis - MM",
    "expense_account": "600 - Achats - MM",
    "discount_account": null,
    "provisional_expense_account": null,
    "cost_center": "Principal - MM",
    "has_serial_no": 0,
    "has_batch_no": 0,
    "batch_no": null,
    "uom": "Unité",
    "stock_uom": "Unité",
    "min_order_qty": "",
    "qty": 1,
    "stock_qty": 1,
    "price_list_rate": 10000,
    "base_price_list_rate": 0,
    "rate": 0,
    "base_rate": 0,
    "amount": 0,
    "base_amount": 0,
    "net_rate": 0,
    "net_amount": 0,
    "discount_percentage": 0,
    "discount_amount": 0,
    "supplier": null,
    "update_stock": 0,
    "delivered_by_supplier": 0,
    "is_fixed_asset": 0,
    "last_purchase_rate": 0,
    "transaction_date": "2022-11-21",
    "against_blanket_order": null,
    "bom_no": null,
    "weight_per_unit": 0,
    "weight_uom": null,
    "grant_commission": 1,
    "is_down_payment_item": 0,
    "down_payment_rate": 0,
    "conversion_factor": 1,
    "item_group": "Produits",
    "brand": null,
    "manufacturer": null,
    "manufacturer_part_no": null,
    "item_tax_rate": "[]",
    "valuation_rate": 0,
    "projected_qty": 0,
    "actual_qty": 0,
    "reserved_qty": 0,
    "has_margin": false,
    "free_item_data": [],
    "child_docname": "c8d5fd227b"
}

Pour les obtenir, il suffit de sélectionner un fournisseur (prix standard 10000 / Remise spécifique au fournisseur de 12%):

Peek 21-11-2022 19-04

Ensuite, on calcule le coût en fonction de la remise (il faudrait enregistrer le type de rÚgle de prix dans des champs séparés et faire le calcul ensuite pour que ce soit moins arbitraire).
Dites moi si ça répond au besoin de la colonne B.

Je regarderai ce qu’on peut faire pour les autres points dùs que j’ai un moment.

Bonne soirée !

Voici le suite du prototype d’hier

Dans les lignes de devis, j’ai ajoutĂ© un bouton Set selling rate pour pouvoir remplir le prix unitaire sur la base du coĂ»t unitaire liĂ© Ă  un fournisseur:
Peek 22-11-2022 11-34

voici le script client avec la logique liée au bouton:

frappe.ui.form.on('Quotation Item', {
	selected_supplier(frm, cdt, cdn) {
	    const item = locals[cdt][cdn]
		frappe.call({
			method: "erpnext.stock.get_item_details.get_item_details",
			args: {
				doc: frm.doc,
				args: {
					item_code: item.item_code,
					barcode: item.barcode,
					serial_no: item.serial_no,
					batch_no: item.batch_no,
					warehouse: item.warehouse,
					supplier: item.selected_supplier,
					currency: frm.doc.currency,
					update_stock: 0,
					conversion_rate: frm.doc.conversion_rate,
					price_list: "Achat Standard",
					price_list_currency: frm.doc.price_list_currency,
					plc_conversion_rate: frm.doc.plc_conversion_rate,
					company: frm.doc.company,
					ignore_pricing_rule: frm.doc.ignore_pricing_rule,
					doctype: frm.doc.doctype,
					name: frm.doc.name,
					project: item.project || frm.doc.project,
					qty: item.qty || 1,
					net_rate: item.rate,
					stock_qty: item.stock_qty,
					conversion_factor: item.conversion_factor,
					weight_per_unit: item.weight_per_unit,
					uom: item.uom,
					weight_uom: item.weight_uom,
					manufacturer: item.manufacturer,
					stock_uom: item.stock_uom,
					cost_center: item.cost_center,
					tax_category: frm.doc.tax_category,
					item_tax_template: item.item_tax_template,
					child_docname: item.name,
					transaction_type: "buying"
				}
			}
		}).then(r => {
		    console.log(r)
	        frappe.model.set_value(item.doctype, item.name, "supplier_price_list_rate", flt(r.message.price_list_rate))
	        frappe.model.set_value(item.doctype, item.name, "unit_cost", flt(r.message.price_list_rate  - (r.message.price_list_rate * (r.message.discount_percentage || 0) / 100)))
		})
	},
	
	set_selling_rate(frm, cdt, cdn) {
	    const item = locals[cdt][cdn]
	    frappe.model.set_value(item.doctype, item.name, "rate", flt(item.unit_cost))
	}
})

Je propose d’ajouter un bouton mais on peut aussi envisager de dĂ©finir le prix au moment oĂč le coĂ»t unitaire est dĂ©terminĂ©. Ça Ă©viterai un clic.

Sur ce prix on ajoute toujours la marge prévue via une rÚgle de prix.

Ensuite pour calculer la marge brute globale, on peut ajouter deux champs dans le corps du devis:

  • Total Costs
  • Margin Percentage

Ensuite on effectue le calcul via un script python dĂ©clenchĂ© Ă  l’évĂ©nement “Avant validation” du devis:

doc.total_costs = 0.0

for item in doc.items:
    doc.total_costs = frappe.utils.flt(doc.total_costs) + frappe.utils.flt(item.unit_cost or item.rate) * frappe.utils.flt(item.qty)
    
doc.margin_percentage = (frappe.utils.flt(doc.net_total) / frappe.utils.flt(doc.total_costs) * 100 ) - 100

=> Pour le pourcentage de main d’oeuvre, il suffit de calculer le coĂ»t global de la main d’oeuvre en filtrant les articles en fonction du code article ou groupe d’article par exemple.

Pour les frais additionnels:

  • Les frais administratifs peuvent ĂȘtre ajoutĂ©s via des articles dĂ©diĂ©s dans les lignes de devis. Il suffit ensuite d’exclure les articles concernĂ©s du prix du chantier.
    Ici j’ai mis une ligne pour simplifier, mais ça peut ĂȘtre sĂ©parĂ© en plusieurs lignes:

  • Les frais supplĂ©mentaires peuvent ĂȘtre ajoutĂ©s via le tableau de taxes et frais pour calculer un pourcentage sur le total HT du devis (ici 7% par exemple):

Voici un exemple de devis reprenant les éléments ci-dessus:



Pour info j’ai enregistrĂ© les prix de deux maniĂšres diffĂ©rentes:

  • Soit dans la liste de prix d’achats pour pouvoir sĂ©lectionner un fournisseur et avoir le coĂ»t rĂ©el associĂ©
  • Soit dans la liste de prix de vente quand ça ne dĂ©pend pas du fournisseur et qu’on veut juste appliquer une marge (exemple: la main d’oeuvre)

N’hĂ©sitez pas Ă  tester et voir ce qui fonctionne pour vous.

Bonne journée

Merci pour tout ça, je vais tester et je vous tiens au courant.
Sinon, j’ai eu un souci avec le calcul du prix des ensembles de produits dans le devis (il ne prend pas en compte la quantitĂ© des Ă©lĂ©ments du sous-ensemble): j’ai ouvert un ticket sur gitlab

en espĂ©rant que c’est bien comme ça qu’il faut faire.

A+

1 « J'aime »

Le dĂ©but fonctionne sauf que je n’arrive pas Ă  rĂ©cupĂ©rer le % de remis, r.message.discount_percentage est toujours Ă  0.

Je dois surement faire une erreur dans la déclaration de ma remise.

Je viens de créer un nouvel article et de nouvelles remises et ça marche, je vais essayer de voir pourquoi ça ne marche pas avec ceux qui existent déjà.
– j’ai bien marquĂ© les case Ă  cocher et remplir et c’est bon

Bonjour,
ça y est, j’ai mis le choses en place, voila le rendu:


A chaque fois on a les couts qui se rajoutent au fur et a mesure au PR. La marge est progressivement rognĂ© jusqu’à la marge totale.
On peut ainsi modifier les PV des articles pour ajuster cette marge (idĂ©alement il faudrait pouvoir indiquer pour la MO le PV souhaitĂ© et mettre Ă  jour automatiquement tous les articles de MO et de mĂȘme sur la marge matĂ©riel)

Je vous joint les différents blocs au cas ou ça peu intéresser et développer la solution de chiffrage.

Ci-dessous aussi le script clients adapté:

frappe.ui.form.on('Quotation Item', {
	pl_selected_supplier(frm, cdt, cdn) {
	    const item = locals[cdt][cdn]
		frappe.call({
			method: "erpnext.stock.get_item_details.get_item_details",
			args: {
				doc: frm.doc,
				args: {
					item_code: item.item_code,
					barcode: item.barcode,
					serial_no: item.serial_no,
					batch_no: item.batch_no,
					warehouse: item.warehouse,
					supplier: item.pl_selected_supplier,
					currency: frm.doc.currency,
					update_stock: 0,
					conversion_rate: frm.doc.conversion_rate,
					price_list: "Achat Standard",
					price_list_currency: frm.doc.price_list_currency,
					plc_conversion_rate: frm.doc.plc_conversion_rate,
					company: frm.doc.company,
					ignore_pricing_rule: frm.doc.ignore_pricing_rule,
					doctype: frm.doc.doctype,
					name: frm.doc.name,
					project: item.project || frm.doc.project,
					qty: item.qty || 1,
					net_rate: item.rate,
					stock_qty: item.stock_qty,
					conversion_factor: item.conversion_factor,
					weight_per_unit: item.weight_per_unit,
					uom: item.uom,
					weight_uom: item.weight_uom,
					manufacturer: item.manufacturer,
					stock_uom: item.stock_uom,
					cost_center: item.cost_center,
					tax_category: frm.doc.tax_category,
					item_tax_template: item.item_tax_template,
					child_docname: item.name,
					transaction_type: "buying"
				}
			}
		}).then(r => {
		    //console.log(r)
	        frappe.model.set_value(item.doctype, item.name, "pl_supplier_price_list_rate", flt(r.message.price_list_rate))
	        frappe.model.set_value(item.doctype, item.name, "pl_supplier_discount_percentage", flt(r.message.discount_percentage || 0))
	        frappe.model.set_value(item.doctype, item.name, "pl_unit_cost", flt(r.message.price_list_rate  - (r.message.price_list_rate * (r.message.discount_percentage || 0) / 100)))
		})
	},
	
	pl_set_selling_rate(frm, cdt, cdn) {
	    const item = locals[cdt][cdn]
	    frappe.model.set_value(item.doctype, item.name, "rate", flt(item.pl_unit_cost))
	}
})

function update_cout_total(frm, cdt, cdn){
    // frm: current ToDo form
    // cdt: child DocType 'Dynamic Link'
    // cdn: child docname (something like 'a6dfk76')
    // cdt and cdn are useful for identifying which row triggered this event
	console.log(frm, cdt, cdn)
	const item=locals[cdt][cdn]
	frappe.model.set_value(item.doctype, item.name, "cout_total", flt(item.cout_horaire*item.nb_heures))
}

frappe.ui.form.on('PL_items_etudes', {
    cout_horaire(frm, cdt, cdn) {
        update_cout_total(frm, cdt, cdn)
	},
	nb_heures(frm, cdt, cdn) {
        update_cout_total(frm, cdt, cdn)
	}
})

frappe.ui.form.on('PL_frais_devis_suppl', {
    pourcentage_pri(frm, cdt, cdn) {
        const item=locals[cdt][cdn]
	    frappe.model.set_value(item.doctype, item.name, "montant_total", flt(frm.doc.pl_pri*item.pourcentage_pri)/100)
	}
})

et le script python:

doc.pl_prc = 0.0
for item in doc.items:
    doc.pl_prc = frappe.utils.flt(doc.pl_prc) + frappe.utils.flt(item.pl_unit_cost or item.rate) * frappe.utils.flt(item.qty)

doc.pl_pri = frappe.utils.flt(doc.pl_prc)
for item in doc.pl_couts_etudes:
    doc.pl_pri = frappe.utils.flt(doc.pl_pri) + frappe.utils.flt(item.cout_total)

doc.pl_prt = frappe.utils.flt(doc.pl_pri)
for item in doc.pl_frais_devis_suppl:
    item.montant_total=doc.pl_pri*item.pourcentage_pri/100
    doc.pl_prt = frappe.utils.flt(doc.pl_prt) + frappe.utils.flt(item.montant_total)
  
doc.pl_margin_rate = frappe.utils.flt(doc.net_total) - frappe.utils.flt(doc.pl_prt)  
doc.pl_total_margin_percentage = frappe.utils.flt(doc.pl_margin_rate) / frappe.utils.flt(doc.net_total) * 100

Et les doctypes personnalisés:


et les 2 nouveaux doctypes:

J’ai une question, pour le moment, les lignes des Ă©tudes et des frais supplĂ©mentaires sont rajoutĂ© Ă  la main dans les tableaux. Comment faire pour avoir les lignes prĂ©remplies Ă  la crĂ©ation d’un devis. Peut-on faire des modĂšles de devis types ?

A trĂšs vite