Multiple delivery dates in Sales Order and make DN based on selected delivery dates (#9933)

* Multiple delivery dates in Sales Order and make DN based on selected delivery dates

* Test case and some other minor fixes

* Updated docs for multi delivery date

* removed the trailing whitespace

* removed the trailing whitespace

* removed trailing whitespace
This commit is contained in:
Nabin Hait 2017-07-20 10:30:59 +05:30 committed by Makarand Bauskar
parent ac9b1332d2
commit 8e0f23efc7
17 changed files with 208 additions and 69 deletions

View File

@ -289,7 +289,7 @@ def get_credit_days(party_type, party, company):
credit_days_based_on, credit_days = \
frappe.db.get_value("Supplier Type", supplier_type, ["credit_days_based_on", "credit_days"])
if not (credit_days_based_on and credit_days):
if not credit_days_based_on:
credit_days_based_on, credit_days = \
frappe.db.get_value("Company", company, ["credit_days_based_on", "credit_days"])

View File

@ -27,7 +27,8 @@ def test_recurring_document(obj, test_records):
base_doc.set(date_field, today)
if base_doc.doctype == "Sales Order":
base_doc.set("delivery_date", add_days(today, 15))
for d in base_doc.get("items"):
d.set("delivery_date", add_days(today, 15))
# monthly
doc1 = frappe.copy_doc(base_doc)

View File

@ -118,7 +118,8 @@ def make_sales_order():
from erpnext.selling.doctype.quotation.quotation import make_sales_order
so = frappe.get_doc(make_sales_order(q))
so.transaction_date = frappe.flags.current_date
so.delivery_date = frappe.utils.add_days(frappe.flags.current_date, 10)
for d in so.get("items"):
d.delivery_date = frappe.utils.add_days(frappe.flags.current_date, 10)
so.insert()
frappe.db.commit()
so.submit()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 443 KiB

View File

@ -31,7 +31,7 @@ Most of the information in your Sales Order is the same as the Quotation.
There are a few amongst other things that a Sales Order will ask you to
update.
* Expected date of delivery.
* Enter delivery date agaist each item. If there are multiple items and if you enter delivery date in the first row, the date will be copied to other rows as well where it is blank.
* Customer Purchase Order number: If your customer has sent you a Purchase Order, you can update its number for future reference (in billing).
### Packing List
@ -89,9 +89,9 @@ ERPNext will automatically create new Order and mail a notification to the Email
Once you “Submit” your Sales Order, you can now trigger different aspects of
your organization:
* To begin purchase click on Make Purchase Request
* To make a shipment entry click on “Make Delivery Note”
* To bill, make Make Sales Invoice
* To begin purchase click on Make -> Purchase Request
* To make a shipment entry click on Make -> Delivery Note. You can also make Delivery Note for selected items based on delivery date.
* To bill, make Make -> Sales Invoice
* To stop further process on this Sales Order, click on “Stop”
### Submission

View File

@ -77,7 +77,6 @@ frappe.ui.form.on("Production Order", {
if (!frm.doc.status)
frm.doc.status = 'Draft';
frm.add_fetch("sales_order", "delivery_date", "expected_delivery_date");
frm.add_fetch("sales_order", "project", "project");
if(frm.doc.__islocal) {

View File

@ -50,8 +50,12 @@ class ProductionOrder(Document):
def validate_sales_order(self):
if self.sales_order:
so = frappe.db.sql("""select name, delivery_date, project from `tabSales Order`
where name=%s and docstatus = 1""", self.sales_order, as_dict=1)
so = frappe.db.sql("""
select so.name, so_item.delivery_date, so.project
from `tabSales Order` so, `tabSales Order Item` so_item
where so.name=%s and so.name=so_item.parent
and so.docstatus = 1 and so_item.item_code=%s
""", (self.sales_order, self.production_item), as_dict=1)
if len(so):
if not self.expected_delivery_date:

View File

@ -421,4 +421,5 @@ erpnext.patches.v8_1.removed_report_support_hours
erpnext.patches.v8_1.add_indexes_in_transaction_doctypes
erpnext.patches.v8_3.set_restrict_to_domain_for_module_def
erpnext.patches.v8_1.update_expense_claim_status
erpnext.patches.v8_3.update_company_total_sales
erpnext.patches.v8_3.update_company_total_sales
erpnext.patches.v8_1.set_delivery_date_in_so_item

View File

@ -0,0 +1,13 @@
import frappe
def execute():
frappe.reload_doctype("Sales Order")
frappe.reload_doctype("Sales Order Item")
frappe.db.sql("""update `tabSales Order` set final_delivery_date = delivery_date where docstatus=1""")
frappe.db.sql("""
update `tabSales Order` so, `tabSales Order Item` so_item
set so_item.delivery_date = so.delivery_date
where so.name = so_item.parent
""")

View File

@ -27,7 +27,8 @@ class TestQuotation(unittest.TestCase):
self.assertEquals(sales_order.get("items")[0].prevdoc_docname, quotation.name)
self.assertEquals(sales_order.customer, "_Test Customer")
sales_order.delivery_date = "2014-01-01"
for d in sales_order.get("items"):
d.delivery_date = "2014-01-01"
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = "2013-05-12"
sales_order.insert()
@ -51,9 +52,11 @@ class TestQuotation(unittest.TestCase):
quotation.submit()
sales_order = make_sales_order(quotation.name)
sales_order.delivery_date = "2016-01-02"
sales_order.naming_series = "_T-Quotation-"
sales_order.transaction_date = "2016-01-01"
for d in sales_order.get("items"):
d.delivery_date = "2016-01-02"
sales_order.insert()
self.assertEquals(quotation.get("items")[0].rate, rate_with_margin)
@ -86,4 +89,4 @@ def get_quotation_dict(customer=None, item_code=None):
'rate': 100
}
]
}
}

View File

@ -33,7 +33,13 @@ frappe.ui.form.on("Sales Order", {
function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" })
erpnext.queries.setup_warehouse_query(frm);
}
},
});
frappe.ui.form.on("Sales Order Item", {
delivery_date: function(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_row(frm.doc, cdt, cdn, "items", "delivery_date");
}
});
erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend({
@ -77,7 +83,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
// delivery note
if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) {
this.frm.add_custom_button(__('Delivery'),
function() { me.make_delivery_note() }, __("Make"));
function() { me.make_delivery_note_based_on_delivery_note(); }, __("Make"));
this.frm.add_custom_button(__('Production Order'),
function() { me.make_production_order() }, __("Make"));
@ -235,7 +241,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
},
order_type: function() {
this.frm.toggle_reqd("delivery_date", this.frm.doc.order_type == "Sales");
this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", this.frm.doc.order_type == "Sales");
},
tc_name: function() {
@ -249,10 +255,72 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
})
},
make_delivery_note_based_on_delivery_note: function() {
var me = this;
var delivery_dates = [];
$.each(this.frm.doc.items || [], function(i, d) {
if(!delivery_dates.includes(d.delivery_date)) {
delivery_dates.push(d.delivery_date);
}
});
var item_grid = this.frm.fields_dict["items"].grid;
if(!item_grid.get_selected().length && delivery_dates.length > 1) {
var dialog = new frappe.ui.Dialog({
title: __("Select Items based on Delivery Date"),
fields: [{fieldtype: "HTML", fieldname: "dates_html"}]
});
var html = $(`
<div style="border: 1px solid #d1d8dd">
<div class="list-item list-item--head">
<div class="list-item__content list-item__content--flex-2">
${__('Delivery Date')}
</div>
</div>
${delivery_dates.map(date => `
<div class="list-item">
<div class="list-item__content list-item__content--flex-2">
<label>
<input type="checkbox" data-date="${date}" checked="checked"/>
${frappe.datetime.str_to_user(date)}
</label>
</div>
</div>
`).join("")}
</div>
`);
var wrapper = dialog.fields_dict.dates_html.$wrapper;
wrapper.html(html);
dialog.set_primary_action(__("Select"), function() {
var dates = wrapper.find('input[type=checkbox]:checked')
.map((i, el) => $(el).attr('data-date')).toArray();
if(!dates) return;
$.each(dates, function(i, d) {
$.each(item_grid.grid_rows || [], function(j, row) {
if(row.doc.delivery_date == d) {
row.doc.__checked = 1;
}
});
})
me.make_delivery_note();
dialog.hide();
});
dialog.show();
} else {
this.make_delivery_note();
}
},
make_delivery_note: function() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
frm: this.frm
frm: me.frm
})
},
@ -344,6 +412,11 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
if(cint(frappe.boot.notification_settings.sales_order)) {
this.frm.email_doc(frappe.boot.notification_settings.sales_order_message);
}
},
items_add: function(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]);
}
});

View File

@ -367,32 +367,29 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.order_type == 'Sales'",
"fieldname": "delivery_date",
"fieldname": "final_delivery_date",
"fieldtype": "Date",
"hidden": 0,
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Delivery Date",
"label": "Final Delivery Date",
"length": 0,
"no_copy": 1,
"oldfieldname": "delivery_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "160px"
"unique": 0
},
{
"allow_bulk_edit": 0,
@ -3632,7 +3629,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-06-19 13:06:31.736384",
"modified": "2017-07-17 18:23:36.023797",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@ -30,7 +30,6 @@ class SalesOrder(SellingController):
self.validate_order_type()
self.validate_delivery_date()
self.validate_mandatory()
self.validate_proj_cust()
self.validate_po()
self.validate_uom_is_integer("stock_uom", "stock_qty")
@ -48,25 +47,20 @@ class SalesOrder(SellingController):
if not self.billing_status: self.billing_status = 'Not Billed'
if not self.delivery_status: self.delivery_status = 'Not Delivered'
def validate_mandatory(self):
# validate transaction date v/s delivery date
if self.delivery_date:
if getdate(self.transaction_date) > getdate(self.delivery_date):
frappe.msgprint(_("Expected Delivery Date is be before Sales Order Date"),
indicator='orange',
title=_('Warning'))
def validate_po(self):
# validate p.o date v/s delivery date
if self.po_date and self.delivery_date and getdate(self.po_date) > getdate(self.delivery_date):
frappe.throw(_("Expected Delivery Date cannot be before Purchase Order Date"))
if self.po_date:
for d in self.get("items"):
if d.delivery_date and getdate(self.po_date) > getdate(d.delivery_date):
frappe.throw(_("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date")
.format(d.idx))
if self.po_no and self.customer:
so = frappe.db.sql("select name from `tabSales Order` \
where ifnull(po_no, '') = %s and name != %s and docstatus < 2\
and customer = %s", (self.po_no, self.name, self.customer))
if so and so[0][0] and not \
cint(frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders")):
if so and so[0][0] and not cint(frappe.db.get_single_value("Selling Settings",
"allow_against_multiple_purchase_orders")):
frappe.msgprint(_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(so[0][0], self.po_no))
def validate_for_items(self):
@ -78,7 +72,7 @@ class SalesOrder(SellingController):
d.transaction_date = self.transaction_date
tot_avail_qty = frappe.db.sql("select projected_qty from `tabBin` \
where item_code = %s and warehouse = %s", (d.item_code,d.warehouse))
where item_code = %s and warehouse = %s", (d.item_code, d.warehouse))
d.projected_qty = tot_avail_qty and flt(tot_avail_qty[0][0]) or 0
# check for same entry multiple times
@ -97,16 +91,30 @@ class SalesOrder(SellingController):
def validate_sales_mntc_quotation(self):
for d in self.get('items'):
if d.prevdoc_docname:
res = frappe.db.sql("select name from `tabQuotation` where name=%s and order_type = %s", (d.prevdoc_docname, self.order_type))
res = frappe.db.sql("select name from `tabQuotation` where name=%s and order_type = %s",
(d.prevdoc_docname, self.order_type))
if not res:
frappe.msgprint(_("Quotation {0} not of type {1}").format(d.prevdoc_docname, self.order_type))
frappe.msgprint(_("Quotation {0} not of type {1}")
.format(d.prevdoc_docname, self.order_type))
def validate_order_type(self):
super(SalesOrder, self).validate_order_type()
def validate_delivery_date(self):
if self.order_type == 'Sales' and not self.delivery_date:
frappe.throw(_("Please enter 'Expected Delivery Date'"))
self.final_delivery_date = None
if self.order_type == 'Sales':
for d in self.get("items"):
if not d.delivery_date:
frappe.throw(_("Row #{0}: Please enter Delivery Date against item {1}")
.format(d.idx, d.item_code))
if getdate(self.transaction_date) > getdate(d.delivery_date):
frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"),
indicator='orange', title=_('Warning'))
if not self.final_delivery_date or \
(d.delivery_date and getdate(d.delivery_date) > getdate(self.final_delivery_date)):
self.final_delivery_date = d.delivery_date
self.validate_sales_mntc_quotation()
@ -122,7 +130,7 @@ class SalesOrder(SellingController):
super(SalesOrder, self).validate_warehouse()
for d in self.get("items"):
if (frappe.db.get_value("Item", d.item_code, "is_stock_item")==1 or
if (frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 or
(self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code))) \
and not d.warehouse and not cint(d.delivered_by_supplier):
frappe.throw(_("Delivery warehouse required for stock item {0}").format(d.item_code),
@ -336,11 +344,14 @@ class SalesOrder(SellingController):
return items
def on_recurring(self, reference_doc):
mcount = month_map[reference_doc.recurring_type]
self.set("delivery_date", get_next_date(reference_doc.delivery_date, mcount,
cint(reference_doc.repeat_on_day_of_month)))
for d in self.get("items"):
reference_delivery_date = frappe.db.get_value("Sales Order Item",
{"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date")
d.set("delivery_date",
get_next_date(reference_delivery_date, mcount, cint(reference_doc.repeat_on_day_of_month)))
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@ -423,7 +434,6 @@ def make_project(source_name, target_doc=None):
},
"field_map":{
"name" : "sales_order",
"delivery_date" : "expected_end_date",
"base_grand_total" : "estimated_costing",
}
},
@ -614,12 +624,17 @@ def get_events(start, end, filters=None):
from frappe.desk.calendar import get_event_conditions
conditions = get_event_conditions("Sales Order", filters)
data = frappe.db.sql("""select name, customer_name, status, delivery_status, billing_status, delivery_date
from `tabSales Order`
where (ifnull(delivery_date, '0000-00-00')!= '0000-00-00') \
and (delivery_date between %(start)s and %(end)s)
and docstatus < 2
{conditions}
data = frappe.db.sql("""
select
so.name, so.customer_name, so.status,
so.delivery_status, so.billing_status, so_item.delivery_date
from
`tabSales Order` so, `tabSales Order Item` so_item
where so.name = so_item.parent
and (ifnull(so_item.delivery_date, '0000-00-00')!= '0000-00-00') \
and (so_item.delivery_date between %(start)s and %(end)s)
and so.docstatus < 2
{conditions}
""".format(conditions=conditions), {
"start": start,
"end": end
@ -659,7 +674,7 @@ def make_purchase_order_for_drop_shipment(source_name, for_supplier, target_doc=
target.run_method("calculate_taxes_and_totals")
def update_item(source, target, source_parent):
target.schedule_date = source_parent.delivery_date
target.schedule_date = source.delivery_date
target.qty = flt(source.qty) - flt(source.ordered_qty)
target.stock_qty = (flt(source.qty) - flt(source.ordered_qty)) * flt(source.conversion_factor)

View File

@ -1,14 +1,14 @@
frappe.listview_settings['Sales Order'] = {
add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date", "per_delivered", "per_billed",
"status", "order_type"],
add_fields: ["base_grand_total", "customer_name", "currency", "final_delivery_date",
"per_delivered", "per_billed", "status", "order_type", "name"],
get_indicator: function(doc) {
if(doc.status==="Closed"){
return [__("Closed"), "green", "status,=,Closed"];
} else if (doc.order_type !== "Maintenance"
&& flt(doc.per_delivered, 2) < 100 && frappe.datetime.get_diff(doc.delivery_date) < 0) {
&& flt(doc.per_delivered, 2) < 100 && frappe.datetime.get_diff(doc.final_delivery_date) < 0) {
// to bill & overdue
return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
return [__("Overdue"), "red", "per_delivered,<,100|final_delivery_date,<,Today|status,!=,Closed"];
} else if (doc.order_type !== "Maintenance"
&& flt(doc.per_delivered, 2) < 100 && doc.status!=="Closed") {

View File

@ -7,7 +7,6 @@
"customer": "_Test Customer",
"customer_group": "_Test Customer Group",
"customer_name": "_Test Customer",
"delivery_date": "2013-02-23",
"doctype": "Sales Order",
"base_grand_total": 1000.0,
"grand_total": 1000.0,
@ -23,6 +22,7 @@
"doctype": "Sales Order Item",
"item_code": "_Test Item Home Desktop 100",
"item_name": "CPU",
"delivery_date": "2013-02-23",
"parentfield": "items",
"qty": 10.0,
"rate": 100.0,

View File

@ -512,7 +512,6 @@ def make_sales_order(**args):
so.company = args.company or "_Test Company"
so.customer = args.customer or "_Test Customer"
so.delivery_date = add_days(so.transaction_date, 10)
so.currency = args.currency or "INR"
if args.selling_price_list:
so.selling_price_list = args.selling_price_list
@ -533,6 +532,9 @@ def make_sales_order(**args):
"rate": args.rate or 100
})
for d in so.get("items"):
d.delivery_date = add_days(so.transaction_date, 10)
if not args.do_not_save:
so.insert()
if not args.do_not_submit:

View File

@ -18,7 +18,7 @@
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 4,
"columns": 3,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
@ -200,6 +200,36 @@
"unique": 0,
"width": "300px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "delivery_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Delivery Date",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -324,7 +354,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"columns": 1,
"fieldname": "qty",
"fieldtype": "Float",
"hidden": 0,
@ -1933,7 +1963,7 @@
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
"modified": "2017-05-10 17:14:48.277982",
"modified": "2017-07-18 18:26:36.870342",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",