fixed conflict

This commit is contained in:
Nabin Hait 2015-02-18 11:55:13 +05:30
commit da5f766dd5
40 changed files with 1305 additions and 1089 deletions

View File

@ -97,7 +97,7 @@ def get_charts_for_country(country):
with open(os.path.join(os.path.dirname(__file__), "syscohada_syscohada_chart_template.json"), "r") as f: with open(os.path.join(os.path.dirname(__file__), "syscohada_syscohada_chart_template.json"), "r") as f:
_get_chart_name(f.read()) _get_chart_name(f.read())
if len(charts) > 1: if len(charts) != 1:
charts.append("Standard") charts.append("Standard")
return charts return charts

View File

@ -39,6 +39,7 @@ class JournalEntry(AccountsController):
self.check_credit_days() self.check_credit_days()
self.validate_expense_claim() self.validate_expense_claim()
self.validate_credit_debit_note() self.validate_credit_debit_note()
self.validate_empty_accounts_table()
def on_submit(self): def on_submit(self):
self.check_credit_limit() self.check_credit_limit()
@ -450,6 +451,10 @@ class JournalEntry(AccountsController):
}) })
if count: if count:
frappe.throw(_("{0} already made against stock entry {1}".format(self.voucher_type, self.stock_entry))) frappe.throw(_("{0} already made against stock entry {1}".format(self.voucher_type, self.stock_entry)))
def validate_empty_accounts_table(self):
if not self.get('accounts'):
frappe.throw("Accounts table cannot be blank.")
@frappe.whitelist() @frappe.whitelist()
def get_default_bank_cash_account(company, voucher_type, mode_of_payment=None): def get_default_bank_cash_account(company, voucher_type, mode_of_payment=None):

View File

@ -1,8 +1,6 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/account_tree_grid.js");
frappe.pages['financial-analytics'].on_page_load = function(wrapper) { frappe.pages['financial-analytics'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({ frappe.ui.make_app_page({
parent: wrapper, parent: wrapper,
@ -10,9 +8,11 @@ frappe.pages['financial-analytics'].on_page_load = function(wrapper) {
single_column: true single_column: true
}); });
erpnext.financial_analytics = new erpnext.FinancialAnalytics(wrapper, 'Financial Analytics'); erpnext.financial_analytics = new erpnext.FinancialAnalytics(wrapper, 'Financial Analytics');
frappe.add_breadcrumbs("Accounts") frappe.add_breadcrumbs("Accounts");
} };
frappe.require("assets/erpnext/js/account_tree_grid.js");
erpnext.FinancialAnalytics = erpnext.AccountTreeGrid.extend({ erpnext.FinancialAnalytics = erpnext.AccountTreeGrid.extend({
filters: [ filters: [

View File

@ -11,10 +11,11 @@ frappe.pages['purchase-analytics'].on_page_load = function(wrapper) {
new erpnext.PurchaseAnalytics(wrapper); new erpnext.PurchaseAnalytics(wrapper);
frappe.add_breadcrumbs("Buying") frappe.add_breadcrumbs("Buying");
} }
frappe.assets.views["Report"]();
erpnext.PurchaseAnalytics = frappe.views.TreeGridReport.extend({ erpnext.PurchaseAnalytics = frappe.views.TreeGridReport.extend({
init: function(wrapper) { init: function(wrapper) {
this._super({ this._super({

View File

@ -60,7 +60,7 @@
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "", "options": "",
"permlevel": 0, "permlevel": 0,
"reqd": 0 "reqd": 1
}, },
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
@ -77,7 +77,7 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2014-12-23 15:01:54.340605", "modified": "2015-02-12 17:49:00.126034",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",

View File

@ -7,7 +7,7 @@ $.extend(cur_frm.cscript, {
cfn_set_fields(doc, dt, dn); cfn_set_fields(doc, dt, dn);
this.frm.add_fetch("sales_order", "delivery_date", "expected_delivery_date"); this.frm.add_fetch("sales_order", "delivery_date", "expected_delivery_date");
if(doc.__islocal) { if(doc.__islocal) {
cur_frm.set_value({ cur_frm.set_value({
"actual_start_date": "", "actual_start_date": "",
@ -70,7 +70,7 @@ $.extend(cur_frm.cscript, {
method: "set_production_order_operations" method: "set_production_order_operations"
}); });
}, },
planned_start_date: function() { planned_start_date: function() {
return this.frm.call({ return this.frm.call({
doc: this.frm.doc, doc: this.frm.doc,
@ -144,7 +144,7 @@ cur_frm.cscript['Unstop Production Order'] = function() {
} }
cur_frm.cscript['Transfer Raw Materials'] = function() { cur_frm.cscript['Transfer Raw Materials'] = function() {
cur_frm.cscript.make_se('Material Transfer'); cur_frm.cscript.make_se('Material Transfer for Manufacture');
} }
cur_frm.cscript['Update Finished Goods'] = function() { cur_frm.cscript['Update Finished Goods'] = function() {
@ -176,7 +176,58 @@ cur_frm.set_query("bom_no", function(doc) {
} else msgprint(__("Please enter Production Item first")); } else msgprint(__("Please enter Production Item first"));
}); });
frappe.ui.form.on("Production Order", "additional_operating_cost", function(frm) {
var calculate_total_cost = function(frm) {
var variable_cost = frm.doc.actual_operating_cost ? flt(frm.doc.actual_operating_cost) : flt(frm.doc.planned_operating_cost) var variable_cost = frm.doc.actual_operating_cost ? flt(frm.doc.actual_operating_cost) : flt(frm.doc.planned_operating_cost)
frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost)) frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost))
}) }
frappe.ui.form.on("Production Order", "additional_operating_cost", function(frm) {
calculate_total_cost(frm);
});
frappe.ui.form.on("Production Order Operation", "workstation", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
frappe.call({
"method": "frappe.client.get",
args: {
doctype: "Workstation",
name: d.workstation
},
callback: function (data) {
frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate);
calculate_cost(frm.doc);
calculate_total_cost(frm);
}
})
});
var calculate_cost = function(doc) {
if (doc.operations){
var op = doc.operations;
doc.planned_operating_cost = 0.0;
for(var i=0;i<op.length;i++) {
planned_operating_cost = flt(flt(op[i].hour_rate) * flt(op[i].time_in_mins) / 60, 2);
frappe.model.set_value('Production Order Operation',op[i].name, "planned_operating_cost", planned_operating_cost);
doc.planned_operating_cost += planned_operating_cost;
}
refresh_field('planned_operating_cost');
}
}
frappe.ui.form.on("Production Order Operation", "time_in_mins", function(frm, cdt, cdn) {
calculate_cost(frm.doc);
calculate_total_cost(frm)
});
var company_filter = function(doc) {
return{
filters: {
'company': doc.company
}
}
}
cur_frm.fields_dict.fg_warehouse.get_query = company_filter
cur_frm.fields_dict.wip_warehouse.get_query = company_filter

View File

@ -210,7 +210,7 @@ class ProductionOrder(Document):
d.status = "Completed" d.status = "Completed"
else: else:
frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'"))
def set_actual_dates(self): def set_actual_dates(self):
if self.get("operations"): if self.get("operations"):
actual_date = frappe.db.sql("""select min(actual_start_time) as start_date, max(actual_end_time) as end_date from `tabProduction Order Operation` actual_date = frappe.db.sql("""select min(actual_start_time) as start_date, max(actual_end_time) as end_date from `tabProduction Order Operation`
@ -220,13 +220,16 @@ class ProductionOrder(Document):
else: else:
self.actual_start_date = None self.actual_start_date = None
self.actual_end_date = None self.actual_end_date = None
def validate_delivery_date(self): def validate_delivery_date(self):
if self.planned_start_date and self.expected_delivery_date and getdate(self.expected_delivery_date) < getdate(self.planned_start_date): if self.docstatus==1:
frappe.throw(_("Expected Delivery Date cannot be greater than Planned Start Date")) if self.planned_start_date and self.expected_delivery_date \
and getdate(self.expected_delivery_date) < getdate(self.planned_start_date):
if self.planned_end_date and self.expected_delivery_date and getdate(self.expected_delivery_date) < getdate(self.planned_end_date): frappe.throw(_("Expected Delivery Date cannot be greater than Planned Start Date"))
frappe.msgprint(_("Production might not be able to finish by the Expected Delivery Date."))
if self.planned_end_date and self.expected_delivery_date \
and getdate(self.expected_delivery_date) < getdate(self.planned_end_date):
frappe.msgprint(_("Production might not be able to finish by the Expected Delivery Date."))
@frappe.whitelist() @frappe.whitelist()
def get_item_details(item): def get_item_details(item):
@ -254,7 +257,7 @@ def make_stock_entry(production_order_id, purpose, qty=None):
stock_entry.use_multi_level_bom = production_order.use_multi_level_bom stock_entry.use_multi_level_bom = production_order.use_multi_level_bom
stock_entry.fg_completed_qty = qty or (flt(production_order.qty) - flt(production_order.produced_qty)) stock_entry.fg_completed_qty = qty or (flt(production_order.qty) - flt(production_order.produced_qty))
if purpose=="Material Transfer": if purpose=="Material Transfer for Manufacture":
stock_entry.to_warehouse = production_order.wip_warehouse stock_entry.to_warehouse = production_order.wip_warehouse
else: else:
stock_entry.from_warehouse = production_order.wip_warehouse stock_entry.from_warehouse = production_order.wip_warehouse

View File

@ -117,7 +117,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 1, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -173,7 +173,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 1, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@ -282,7 +282,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"modified": "2014-12-23 15:42:34.892964", "modified": "2015-02-13 16:26:30.441657",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Order Operation", "name": "Production Order Operation",

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and from frappe.utils import cstr, flt, cint, nowdate, now, add_days, comma_and
from frappe import msgprint, _ from frappe import msgprint, _
@ -218,6 +218,9 @@ class ProductionPlanningTool(Document):
for key in items: for key in items:
pro = frappe.new_doc("Production Order") pro = frappe.new_doc("Production Order")
pro.update(items[key]) pro.update(items[key])
pro.planned_start_date = now()
pro.set_production_order_operations()
frappe.flags.mute_messages = True frappe.flags.mute_messages = True
try: try:

View File

@ -113,3 +113,7 @@ erpnext.patches.v5_0.update_item_name_in_bom
erpnext.patches.v5_0.rename_customer_issue erpnext.patches.v5_0.rename_customer_issue
erpnext.patches.v5_0.rename_total_fields erpnext.patches.v5_0.rename_total_fields
erpnext.patches.v5_0.replace_renamed_fields_in_custom_script_and_print_formats erpnext.patches.v5_0.replace_renamed_fields_in_custom_script_and_print_formats
erpnext.patches.v5_0.new_crm_module
erpnext.patches.v5_0.rename_customer_issue
erpnext.patches.v5_0.update_material_transfer_for_manufacture
erpnext.patches.v5_0.manufacturing_activity_type

View File

@ -0,0 +1,13 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
if not frappe.db.exists('Activity Type','Manufacturing') {
doc = frappe.new_doc('Activity Type')
doc.update({
'activity_type' : 'Manufacturing'
})
doc.save()
}

View File

@ -0,0 +1,24 @@
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import frappe
def execute():
frappe.reload_doc('crm', 'doctype', 'lead')
frappe.reload_doc('crm', 'doctype', 'opportunity')
add_crm_to_user_desktop_items()
def add_crm_to_user_desktop_items():
key = "_user_desktop_items"
for user in frappe.get_all("User", filters={"enabled": 1, "user_type": "System User"}):
user = user.name
user_desktop_items = frappe.db.get_defaults(key, parent=user)
if user_desktop_items:
user_desktop_items = json.loads(user_desktop_items)
if "CRM" not in user_desktop_items:
user_desktop_items.append("CRM")
frappe.db.set_default(key, json.dumps(user_desktop_items), parent=user)

View File

@ -0,0 +1,5 @@
import frappe
def execute():
frappe.db.sql("""update `tabStock Entry` set purpose='Material Transfer for Manufacture'
where ifnull(production_order, '')!='' and purpose='Material Transfer'""")

View File

@ -7,4 +7,10 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class ActivityType(Document): class ActivityType(Document):
pass
def on_trash(self):
self.validate_manufacturing_type()
def validate_manufacturing_type(self):
if self.activity_type == 'Manufacturing':
frappe.throw(_("Activity Type 'Manufacturing' cannot be deleted."))

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
frappe.assets.views["Report"]();
erpnext.AccountTreeGrid = frappe.views.TreeGridReport.extend({ erpnext.AccountTreeGrid = frappe.views.TreeGridReport.extend({
init: function(wrapper, title) { init: function(wrapper, title) {
this._super({ this._super({

View File

@ -1,6 +1,8 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.assets.views["Report"]();
erpnext.StockGridReport = frappe.views.TreeGridReport.extend({ erpnext.StockGridReport = frappe.views.TreeGridReport.extend({
get_item_warehouse: function(warehouse, item) { get_item_warehouse: function(warehouse, item) {
if(!this.item_warehouse[item]) this.item_warehouse[item] = {}; if(!this.item_warehouse[item]) this.item_warehouse[item] = {};

View File

@ -12,7 +12,9 @@ frappe.pages['sales-analytics'].on_page_load = function(wrapper) {
frappe.add_breadcrumbs("Selling") frappe.add_breadcrumbs("Selling")
} };
frappe.assets.views["Report"]();
erpnext.SalesAnalytics = frappe.views.TreeGridReport.extend({ erpnext.SalesAnalytics = frappe.views.TreeGridReport.extend({
init: function(wrapper) { init: function(wrapper) {

View File

@ -135,7 +135,7 @@ def install(country=None):
{'doctype': 'Activity Type', 'activity_type': _('Proposal Writing')}, {'doctype': 'Activity Type', 'activity_type': _('Proposal Writing')},
{'doctype': 'Activity Type', 'activity_type': _('Execution')}, {'doctype': 'Activity Type', 'activity_type': _('Execution')},
{'doctype': 'Activity Type', 'activity_type': _('Communication')}, {'doctype': 'Activity Type', 'activity_type': _('Communication')},
{'doctype': 'Activity Type', 'activity_type': _('Manufacturing')}, {'doctype': 'Activity Type', 'activity_type': 'Manufacturing'},
# Industry Type # Industry Type
{'doctype': 'Industry Type', 'industry': _('Accounting')}, {'doctype': 'Industry Type', 'industry': _('Accounting')},

View File

@ -83,11 +83,11 @@ erpnext.wiz.Wizard = Class.extend({
this.hide_current_slide(); this.hide_current_slide();
this.current_slide = this.slide_dict[id]; this.current_slide = this.slide_dict[id];
this.current_slide.$wrapper.toggle(true); this.current_slide.$wrapper.removeClass("hidden");
}, },
hide_current_slide: function() { hide_current_slide: function() {
if(this.current_slide) { if(this.current_slide) {
this.current_slide.$wrapper.toggle(false); this.current_slide.$wrapper.addClass("hidden");
this.current_slide = null; this.current_slide = null;
} }
}, },
@ -103,7 +103,7 @@ erpnext.wiz.Wizard = Class.extend({
erpnext.wiz.WizardSlide = Class.extend({ erpnext.wiz.WizardSlide = Class.extend({
init: function(opts) { init: function(opts) {
$.extend(this, opts); $.extend(this, opts);
this.$wrapper = $("<div>") this.$wrapper = $('<div class="slide-wrapper hidden"></div>')
.appendTo(this.wiz.parent) .appendTo(this.wiz.parent)
.attr("data-slide-id", this.id); .attr("data-slide-id", this.id);
}, },
@ -305,10 +305,11 @@ $.extend(erpnext.wiz, {
$timezone.empty(); $timezone.empty();
// add country specific timezones first // add country specific timezones first
if(country){ if(country) {
var timezone_list = data.country_info[country].timezones || []; var timezone_list = data.country_info[country].timezones || [];
$timezone.add_options(timezone_list.sort()); $timezone.add_options(timezone_list.sort());
slide.get_field("currency").set_input(data.country_info[country].currency); slide.get_field("currency").set_input(data.country_info[country].currency);
slide.get_field("currency").$input.trigger("change");
} }
// add all timezones at the end, so that user has the option to change it to any timezone // add all timezones at the end, so that user has the option to change it to any timezone
@ -320,6 +321,23 @@ $.extend(erpnext.wiz, {
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format
|| "dd-mm-yyyy"); || "dd-mm-yyyy");
}); });
slide.get_input("currency").on("change", function() {
var currency = slide.get_input("currency").val();
frappe.model.with_doc("Currency", currency, function() {
frappe.provide("locals.:Currency." + currency);
var currency_doc = frappe.model.get_doc("Currency", currency);
var number_format = currency_doc.number_format;
if (number_format==="#.###") {
number_format = "#.###,##";
} else if (number_format==="#,###") {
number_format = "#,###.##"
}
frappe.boot.sysdefaults.number_format = number_format;
locals[":Currency"][currency] = $.extend({}, currency_doc);
});
});
} }
}, },
@ -585,14 +603,7 @@ $.extend(erpnext.wiz, {
callback: function(r) { callback: function(r) {
wiz.show_complete(); wiz.show_complete();
setTimeout(function() { setTimeout(function() {
if(user==="Administrator") { window.location = "/desk";
msgprint(__("Login with your new User ID") + ": " + values.email);
setTimeout(function() {
frappe.app.logout();
}, 2000);
} else {
window.location = "/desk";
}
}, 2000); }, 2000);
}, },
error: function(r) { error: function(r) {

View File

@ -75,6 +75,8 @@ def setup_account(args=None):
website_maker(args.company_name.strip(), args.company_tagline, args.name) website_maker(args.company_name.strip(), args.company_tagline, args.name)
create_logo(args) create_logo(args)
login_as_first_user(args)
frappe.clear_cache() frappe.clear_cache()
frappe.db.commit() frappe.db.commit()
@ -432,6 +434,11 @@ def create_territories():
"is_group": "No" "is_group": "No"
}).insert() }).insert()
def login_as_first_user(args):
if args.get("email") and hasattr(frappe.local, "login_manager"):
frappe.local.login_manager.user = args.get("email")
frappe.local.login_manager.post_login()
@frappe.whitelist() @frappe.whitelist()
def load_messages(language): def load_messages(language):
frappe.clear_cache() frappe.clear_cache()
@ -444,8 +451,6 @@ def load_messages(language):
@frappe.whitelist() @frappe.whitelist()
def load_languages(): def load_languages():
from frappe.sessions import get_geo_from_ip
return { return {
"default_language": get_language_from_code(frappe.local.lang), "default_language": get_language_from_code(frappe.local.lang),
"languages": sorted(get_lang_dict().keys()) "languages": sorted(get_lang_dict().keys())

View File

@ -1,12 +1,13 @@
frappe.listview_settings['Material Request'] = { frappe.listview_settings['Material Request'] = {
add_fields: ["material_request_type", "status", "per_ordered"], add_fields: ["material_request_type", "status", "per_ordered"],
get_status: function(doc) { get_indicator: function(doc) {
console.log()
if(doc.status=="Stopped") { if(doc.status=="Stopped") {
return [__("Stopped"), "red", "status,=,Stopped"]; return [__("Stopped"), "red", "status,=,Stopped"];
} if(doc.docstatus==1 && doc.per_ordered < 100) { } else if(doc.docstatus==1 && doc.per_ordered < 100) {
return [__("Pending"), "orange", "per_ordered,<,100"]; return [__("Pending"), "orange", "per_ordered,<,100"];
} else if(doc.status==1 && doc.per_ordered == 100) { } else if(doc.docstatus==1 && doc.per_ordered == 100) {
return [__("Completed"), "green", "per_ordered,=,100"]; return [__("Ordered"), "green", "per_ordered,=,100"];
} }
} }
}; };

View File

@ -27,6 +27,7 @@
"fieldname": "price_list_name", "fieldname": "price_list_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Price List Name", "label": "Price List Name",
"no_copy": 1,
"oldfieldname": "price_list_name", "oldfieldname": "price_list_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0, "permlevel": 0,
@ -74,7 +75,7 @@
"icon": "icon-tags", "icon": "icon-tags",
"idx": 1, "idx": 1,
"max_attachments": 1, "max_attachments": 1,
"modified": "2015-02-05 05:11:42.450750", "modified": "2015-02-12 17:39:02.825767",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Price List", "name": "Price List",

View File

@ -286,7 +286,7 @@ class PurchaseReceipt(BuyingController):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse): if warehouse_account.get(d.warehouse):
val_rate_db_precision = 6 if cint(self.precision("valuation_rate")) <= 6 else 9 val_rate_db_precision = 6 if cint(d.precision("valuation_rate")) <= 6 else 9
# warehouse account # warehouse account
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({

View File

@ -122,7 +122,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
clean_up: function() { clean_up: function() {
// Clear Production Order record from locals, because it is updated via Stock Entry // Clear Production Order record from locals, because it is updated via Stock Entry
if(this.frm.doc.production_order && if(this.frm.doc.production_order &&
this.frm.doc.purpose == "Manufacture") { in_list(["Manufacture", "Material Transfer for Manufacture"], this.frm.doc.purpose)) {
frappe.model.remove_from_locals("Production Order", frappe.model.remove_from_locals("Production Order",
this.frm.doc.production_order); this.frm.doc.production_order);
} }
@ -164,7 +164,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
}, },
toggle_enable_bom: function() { toggle_enable_bom: function() {
this.frm.toggle_enable("bom_no", this.frm.doc.purpose!="Manufacture"); this.frm.toggle_enable("bom_no", !in_list(["Manufacture", "Material Transfer for Manufacture"], this.frm.doc.purpose));
}, },
get_doctype_docname: function() { get_doctype_docname: function() {
@ -233,8 +233,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
}, },
source_mandatory: ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract"], source_mandatory: ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract",
target_mandatory: ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract"], "Material Transfer for Manufacture"],
target_mandatory: ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract",
"Material Transfer for Manufacture"],
from_warehouse: function(doc) { from_warehouse: function(doc) {
var me = this; var me = this;
@ -251,6 +253,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
}, },
set_warehouse_if_missing: function(fieldname, value, condition) { set_warehouse_if_missing: function(fieldname, value, condition) {
var changed = false;
for (var i=0, l=(this.frm.doc.items || []).length; i<l; i++) { for (var i=0, l=(this.frm.doc.items || []).length; i<l; i++) {
var row = this.frm.doc.items[i]; var row = this.frm.doc.items[i];
if (!row[fieldname]) { if (!row[fieldname]) {
@ -259,8 +262,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
} }
frappe.model.set_value(row.doctype, row.name, fieldname, value, "Link"); frappe.model.set_value(row.doctype, row.name, fieldname, value, "Link");
changed = true;
} }
} }
refresh_field("items");
}, },
items_on_form_rendered: function(doc, grid_row) { items_on_form_rendered: function(doc, grid_row) {

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ from frappe.utils import cstr, cint, flt, comma_or, nowdate
from frappe import _ from frappe import _
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center, get_conversion_factor from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center, get_conversion_factor
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
@ -82,7 +82,7 @@ class StockEntry(StockController):
self.meta.get_label("posting_date")) self.meta.get_label("posting_date"))
def validate_purpose(self): def validate_purpose(self):
valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture",
"Manufacture", "Repack", "Subcontract", "Sales Return", "Purchase Return"] "Manufacture", "Repack", "Subcontract", "Sales Return", "Purchase Return"]
if self.purpose not in valid_purposes: if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
@ -112,7 +112,7 @@ class StockEntry(StockController):
if not item.transfer_qty: if not item.transfer_qty:
item.transfer_qty = item.qty * item.conversion_factor item.transfer_qty = item.qty * item.conversion_factor
if (self.purpose in ("Material Transfer", "Sales Return", "Purchase Return") if (self.purpose in ("Material Transfer", "Sales Return", "Purchase Return", "Material Transfer for Manufacture")
and not item.serial_no and not item.serial_no
and item.item_code in serialized_items): and item.item_code in serialized_items):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
@ -121,8 +121,8 @@ class StockEntry(StockController):
def validate_warehouse(self, pro_obj): def validate_warehouse(self, pro_obj):
"""perform various (sometimes conditional) validations on warehouse""" """perform various (sometimes conditional) validations on warehouse"""
source_mandatory = ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract"] source_mandatory = ["Material Issue", "Material Transfer", "Purchase Return", "Subcontract", "Material Transfer for Manufacture"]
target_mandatory = ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract"] target_mandatory = ["Material Receipt", "Material Transfer", "Sales Return", "Subcontract", "Material Transfer for Manufacture"]
validate_for_manufacture_repack = any([d.bom_no for d in self.get("items")]) validate_for_manufacture_repack = any([d.bom_no for d in self.get("items")])
@ -169,13 +169,14 @@ class StockEntry(StockController):
frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx)) frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx))
def validate_production_order(self): def validate_production_order(self):
if self.purpose == "Manufacture": if self.purpose in ("Manufacture", "Material Transfer for Manufacture"):
# check if production order is entered # check if production order is entered
if not self.production_order: if not self.production_order:
frappe.throw(_("Production order number is mandatory for stock entry purpose manufacture")) frappe.throw(_("Production order number is mandatory for stock entry purpose manufacture"))
# check for double entry # check for double entry
self.check_if_operations_completed() if self.purpose=="Manufacture":
self.check_duplicate_entry_for_production_order() self.check_if_operations_completed()
self.check_duplicate_entry_for_production_order()
elif self.purpose != "Material Transfer": elif self.purpose != "Material Transfer":
self.production_order = None self.production_order = None
@ -254,7 +255,7 @@ class StockEntry(StockController):
if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and d.actual_qty < d.transfer_qty: if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and d.actual_qty < d.transfer_qty:
frappe.throw(_("""Row {0}: Qty not avalable in warehouse {1} on {2} {3}. frappe.throw(_("""Row {0}: Qty not avalable in warehouse {1} on {2} {3}.
Available Qty: {4}, Transfer Qty: {5}""").format(d.idx, d.s_warehouse, Available Qty: {4}, Transfer Qty: {5}""").format(d.idx, d.s_warehouse,
self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty)) self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty), NegativeStockError)
# get incoming rate # get incoming rate
if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force: if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force:
@ -285,7 +286,7 @@ class StockEntry(StockController):
if flt(d.completed_qty): if flt(d.completed_qty):
operation_cost_per_unit += flt(d.actual_operating_cost) / flt(d.completed_qty) operation_cost_per_unit += flt(d.actual_operating_cost) / flt(d.completed_qty)
else: else:
operation_cost_per_unit += flt(d.planned_operating_cost) / flt(self.qty) operation_cost_per_unit += flt(d.planned_operating_cost) / flt(pro_order.qty)
if not operation_cost_per_unit and bom_no: if not operation_cost_per_unit and bom_no:
bom = frappe.db.get_value("BOM", bom_no, ["operating_cost", "quantity"], as_dict=1) bom = frappe.db.get_value("BOM", bom_no, ["operating_cost", "quantity"], as_dict=1)
@ -503,8 +504,8 @@ class StockEntry(StockController):
if self.bom_no: if self.bom_no:
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
"Subcontract"]: "Subcontract", "Material Transfer for Manufacture"]:
if self.production_order and self.purpose == "Material Transfer": if self.production_order and self.purpose == "Material Transfer for Manufacture":
item_dict = self.get_pending_raw_materials(pro_obj) item_dict = self.get_pending_raw_materials(pro_obj)
if self.to_warehouse and pro_obj: if self.to_warehouse and pro_obj:
for item in item_dict.values(): for item in item_dict.values():
@ -601,7 +602,7 @@ class StockEntry(StockController):
result = frappe.db.sql("""select t1.item_code, sum(t1.qty) result = frappe.db.sql("""select t1.item_code, sum(t1.qty)
from `tabStock Entry Detail` t1, `tabStock Entry` t2 from `tabStock Entry Detail` t1, `tabStock Entry` t2
where t1.parent = t2.name and t2.production_order = %s and t2.docstatus = 1 where t1.parent = t2.name and t2.production_order = %s and t2.docstatus = 1
and t2.purpose = 'Material Transfer' and t2.purpose = 'Material Transfer for Manufacture'
group by t1.item_code""", self.production_order) group by t1.item_code""", self.production_order)
for t in result: for t in result:
issued_item_qty[t[0]] = flt(t[1]) issued_item_qty[t[0]] = flt(t[1])
@ -662,7 +663,7 @@ def get_party_details(ref_dt, ref_dn):
def get_production_order_details(production_order): def get_production_order_details(production_order):
res = frappe.db.sql("""select bom_no, use_multi_level_bom, wip_warehouse, res = frappe.db.sql("""select bom_no, use_multi_level_bom, wip_warehouse,
ifnull(qty, 0) - ifnull(produced_qty, 0) as fg_completed_qty, ifnull(qty, 0) - ifnull(produced_qty, 0) as fg_completed_qty,
(infull(additional_operating_cost, 0) / qty)*(ifnull(qty, 0) - ifnull(produced_qty, 0)) as additional_operating_cost (ifnull(additional_operating_cost, 0) / qty)*(ifnull(qty, 0) - ifnull(produced_qty, 0)) as additional_operating_cost
from `tabProduction Order` where name = %s""", production_order, as_dict=1) from `tabProduction Order` where name = %s""", production_order, as_dict=1)
return res and res[0] or {} return res and res[0] or {}

View File

@ -5,6 +5,26 @@ frappe.require("assets/erpnext/js/controllers/stock_controller.js");
frappe.require("assets/erpnext/js/utils.js"); frappe.require("assets/erpnext/js/utils.js");
frappe.provide("erpnext.stock"); frappe.provide("erpnext.stock");
frappe.ui.form.on("Stock Reconciliation", "get_items", function(frm) {
frappe.prompt({label:"Warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1},
function(data) {
frappe.call({
method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
args: {warehouse: data.warehouse},
callback: function(r) {
var items = [];
frm.clear_table("items");
for(var i=0; i< r.message.length; i++) {
var d = frm.add_child("items");
$.extend(d, r.message[i]);
}
frm.refresh_field("items");
}
});
}
, __("Get Items"), __("Update"));
});
erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({
onload: function() { onload: function() {
this.set_default_expense_account(); this.set_default_expense_account();
@ -31,6 +51,8 @@ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({
setup: function() { setup: function() {
var me = this; var me = this;
this.frm.get_docfield("items").allow_bulk_edit = 1;
if (sys_defaults.auto_accounting_for_stock) { if (sys_defaults.auto_accounting_for_stock) {
this.frm.add_fetch("company", "stock_adjustment_account", "expense_account"); this.frm.add_fetch("company", "stock_adjustment_account", "expense_account");
this.frm.add_fetch("company", "cost_center", "cost_center"); this.frm.add_fetch("company", "cost_center", "cost_center");
@ -55,108 +77,12 @@ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({
}, },
refresh: function() { refresh: function() {
if(this.frm.doc.docstatus===0) { if(this.frm.doc.docstatus==1) {
this.show_download_template();
this.show_upload();
if(this.frm.doc.reconciliation_json) {
this.frm.set_intro(__("You can submit this Stock Reconciliation."));
} else {
this.frm.set_intro(__("Download the Template, fill appropriate data and attach the modified file."));
}
} else if(this.frm.doc.docstatus == 1) {
this.frm.set_intro(__("Cancelling this Stock Reconciliation will nullify its effect."));
this.show_stock_ledger(); this.show_stock_ledger();
this.show_general_ledger(); this.show_general_ledger();
} else {
this.frm.set_intro("");
}
this.show_reconciliation_data();
this.show_download_reconciliation_data();
},
show_download_template: function() {
var me = this;
this.frm.add_custom_button(__("Download Template"), function() {
this.title = __("Stock Reconcilation Template");
frappe.tools.downloadify([[__("Stock Reconciliation")],
["----"],
[__("Stock Reconciliation can be used to update the stock on a particular date, usually as per physical inventory.")],
[__("When submitted, the system creates difference entries to set the given stock and valuation on this date.")],
[__("It can also be used to create opening stock entries and to fix stock value.")],
["----"],
[__("Notes:")],
[__("Item Code and Warehouse should already exist.")],
[__("You can update either Quantity or Valuation Rate or both.")],
[__("If no change in either Quantity or Valuation Rate, leave the cell blank.")],
["----"],
["Item Code", "Warehouse", "Quantity", "Valuation Rate"]], null, this);
return false;
}, "icon-download");
},
show_upload: function() {
var me = this;
var $wrapper = $(cur_frm.fields_dict.upload_html.wrapper).empty();
// upload
frappe.upload.make({
parent: $wrapper,
args: {
method: 'erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.upload'
},
sample_url: "e.g. http://example.com/somefile.csv",
callback: function(attachment, r) {
me.frm.set_value("reconciliation_json", JSON.stringify(r.message));
me.show_reconciliation_data();
me.frm.save();
}
});
// rename button
$wrapper.find('form input[type="submit"]')
.attr('value', 'Upload')
},
show_download_reconciliation_data: function() {
var me = this;
if(this.frm.doc.reconciliation_json) {
this.frm.add_custom_button(__("Download Reconcilation Data"), function() {
this.title = __("Stock Reconcilation Data");
frappe.tools.downloadify(JSON.parse(me.frm.doc.reconciliation_json), null, this);
return false;
}, "icon-download", "btn-default");
} }
}, },
show_reconciliation_data: function() {
var $wrapper = $(cur_frm.fields_dict.reconciliation_html.wrapper).empty();
if(this.frm.doc.reconciliation_json) {
var reconciliation_data = JSON.parse(this.frm.doc.reconciliation_json);
var _make = function(data, header) {
var result = "";
var _render = header
? function(col) { return "<th>" + col + "</th>"; }
: function(col) { return "<td>" + col + "</td>"; };
$.each(data, function(i, row) {
result += "<tr>"
+ $.map(row, _render).join("")
+ "</tr>";
});
return result;
};
var $reconciliation_table = $("<div style='overflow-x: auto;'>\
<table class='table table-striped table-bordered'>\
<thead>" + _make([reconciliation_data[0]], true) + "</thead>\
<tbody>" + _make(reconciliation_data.splice(1)) + "</tbody>\
</table>\
</div>").appendTo($wrapper);
}
},
}); });
cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm});
@ -167,4 +93,4 @@ cur_frm.cscript.company = function(doc, cdt, cdn) {
cur_frm.cscript.posting_date = function(doc, cdt, cdn){ cur_frm.cscript.posting_date = function(doc, cdt, cdn){
erpnext.get_fiscal_year(doc.company, doc.posting_date); erpnext.get_fiscal_year(doc.company, doc.posting_date);
} }

View File

@ -1,5 +1,5 @@
{ {
"allow_copy": 1, "allow_copy": 0,
"autoname": "SR/.######", "autoname": "SR/.######",
"creation": "2013-03-28 10:35:31", "creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
@ -31,6 +31,14 @@
"read_only": 0, "read_only": 0,
"reqd": 1 "reqd": 1
}, },
{
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"permlevel": 0
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@ -42,6 +50,11 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "col1",
"fieldtype": "Column Break",
"permlevel": 0
},
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
@ -59,14 +72,6 @@
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
}, },
{
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"permlevel": 0
},
{ {
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)", "depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "cost_center", "fieldname": "cost_center",
@ -76,9 +81,31 @@
"permlevel": 0 "permlevel": 0
}, },
{ {
"fieldname": "col1", "fieldname": "sb9",
"fieldtype": "Column Break", "fieldtype": "Section Break",
"permlevel": 0 "permlevel": 0,
"precision": ""
},
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Stock Reconciliation Item",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Items",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"permlevel": 0,
"precision": ""
}, },
{ {
"fieldname": "upload_html", "fieldname": "upload_html",
@ -119,7 +146,7 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"max_attachments": 1, "max_attachments": 1,
"modified": "2015-02-05 05:11:47.153367", "modified": "2015-02-17 02:09:17.483016",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reconciliation", "name": "Stock Reconciliation",

View File

@ -4,7 +4,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import frappe.defaults import frappe.defaults
import json
from frappe import msgprint, _ from frappe import msgprint, _
from frappe.utils import cstr, flt, cint from frappe.utils import cstr, flt, cint
from erpnext.stock.stock_ledger import update_entries_after from erpnext.stock.stock_ledger import update_entries_after
@ -28,64 +27,52 @@ class StockReconciliation(StockController):
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
def validate_data(self): def validate_data(self):
if not self.reconciliation_json:
return
data = json.loads(self.reconciliation_json)
# strip out extra columns (if any)
data = [row[:4] for row in data]
if self.head_row not in data:
msgprint(_("""Wrong Template: Unable to find head row."""),
raise_exception=1)
# remove the help part and save the json
head_row_no = 0
if data.index(self.head_row) != 0:
head_row_no = data.index(self.head_row)
data = data[head_row_no:]
self.reconciliation_json = json.dumps(data)
def _get_msg(row_num, msg): def _get_msg(row_num, msg):
return _("Row # {0}: ").format(row_num+head_row_no+2) + msg return _("Row # {0}: ").format(row_num+1) + msg
self.validation_messages = [] self.validation_messages = []
item_warehouse_combinations = [] item_warehouse_combinations = []
default_currency = frappe.db.get_default("currency")
# validate no of rows # validate no of rows
rows = data[1:] if len(self.items) > 100:
if len(rows) > 100: frappe.throw(_("""Max 100 rows for Stock Reconciliation."""))
msgprint(_("""Sorry! We can only allow upto 100 rows for Stock Reconciliation."""), for row_num, row in enumerate(self.items):
raise_exception=True)
for row_num, row in enumerate(rows):
# find duplicates # find duplicates
if [row[0], row[1]] in item_warehouse_combinations: if [row.item_code, row.warehouse] in item_warehouse_combinations:
self.validation_messages.append(_get_msg(row_num, _("Duplicate entry"))) self.validation_messages.append(_get_msg(row_num, _("Duplicate entry")))
else: else:
item_warehouse_combinations.append([row[0], row[1]]) item_warehouse_combinations.append([row.item_code, row.warehouse])
self.validate_item(row[0], row_num+head_row_no+2) self.validate_item(row.item_code, row_num+1)
# validate warehouse # validate warehouse
if not frappe.db.get_value("Warehouse", row[1]): if not frappe.db.get_value("Warehouse", row.warehouse):
self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system"))) self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system")))
# if both not specified # if both not specified
if row[2] in ["", None] and row[3] in ["", None]: if row.qty in ["", None] and row.valuation_rate in ["", None]:
self.validation_messages.append(_get_msg(row_num, self.validation_messages.append(_get_msg(row_num,
_("Please specify either Quantity or Valuation Rate or both"))) _("Please specify either Quantity or Valuation Rate or both")))
# do not allow negative quantity # do not allow negative quantity
if flt(row[2]) < 0: if flt(row.qty) < 0:
self.validation_messages.append(_get_msg(row_num, self.validation_messages.append(_get_msg(row_num,
_("Negative Quantity is not allowed"))) _("Negative Quantity is not allowed")))
# do not allow negative valuation # do not allow negative valuation
if flt(row[3]) < 0: if flt(row.valuation_rate) < 0:
self.validation_messages.append(_get_msg(row_num, self.validation_messages.append(_get_msg(row_num,
_("Negative Valuation Rate is not allowed"))) _("Negative Valuation Rate is not allowed")))
if row.qty and not row.valuation_rate:
# try if there is a buying price list in default currency
buying_rate = frappe.db.get_value("Item Price", {"item_code": row.item_code,
"buying": 1, "currency": default_currency}, "price_list_rate")
if buying_rate:
row.valuation_rate = buying_rate
# throw all validation messages # throw all validation messages
if self.validation_messages: if self.validation_messages:
for msg in self.validation_messages: for msg in self.validation_messages:
@ -129,16 +116,7 @@ class StockReconciliation(StockController):
and create stock ledger entries based on the difference""" and create stock ledger entries based on the difference"""
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
row_template = ["item_code", "warehouse", "qty", "valuation_rate"] for row in self.items:
if not self.reconciliation_json:
msgprint(_("""Stock Reconciliation file not uploaded"""), raise_exception=1)
data = json.loads(self.reconciliation_json)
for row_num, row in enumerate(data[data.index(self.head_row)+1:]):
row = frappe._dict(zip(row_template, row))
row["row_num"] = row_num
if row.qty in ("", None) or row.valuation_rate in ("", None): if row.qty in ("", None) or row.valuation_rate in ("", None):
previous_sle = get_previous_sle({ previous_sle = get_previous_sle({
"item_code": row.item_code, "item_code": row.item_code,
@ -216,7 +194,14 @@ class StockReconciliation(StockController):
frappe.throw(_("Difference Account must be a 'Liability' type account, since this Stock Reconciliation is an Opening Entry")) frappe.throw(_("Difference Account must be a 'Liability' type account, since this Stock Reconciliation is an Opening Entry"))
@frappe.whitelist() @frappe.whitelist()
def upload(): def get_items(warehouse):
from frappe.utils.csvutils import read_csv_content_from_uploaded_file from erpnext.stock.utils import get_stock_balance
csv_content = read_csv_content_from_uploaded_file() items = frappe.get_list("Item", fields=["name"], filters=
return filter(lambda x: x and any(x), csv_content) {"is_stock_item": "Yes", "has_serial_no": "No", "has_batch_no": "No"})
for item in items:
item.item_code = item.name
item.warehouse = warehouse
del item["name"]
item.qty, item.valuation_rate = get_stock_balance(item.name, warehouse, with_valuation_rate=True)
return items

View File

@ -0,0 +1,110 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"creation": "2015-02-17 01:06:05.072764",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"fields": [
{
"allow_on_submit": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Item Code",
"no_copy": 0,
"options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0
},
{
"allow_on_submit": 0,
"fieldname": "warehouse",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Warehouse",
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0
},
{
"allow_on_submit": 0,
"description": "Leave blank if no change",
"fieldname": "qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Quantity",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0
},
{
"allow_on_submit": 0,
"description": "Leave blank if no change",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Valuation Rate",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"modified": "2015-02-17 01:07:50.200649",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class StockReconciliationItem(Document):
pass

View File

@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.pages['stock-analytics'].on_page_load = function(wrapper) { frappe.pages['stock-analytics'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({ frappe.ui.make_app_page({
parent: wrapper, parent: wrapper,
title: __('Stock Analytics'), title: __('Stock Analytics'),
@ -13,7 +13,8 @@ frappe.pages['stock-analytics'].on_page_load = function(wrapper) {
frappe.add_breadcrumbs("Stock") frappe.add_breadcrumbs("Stock")
}
frappe.require("assets/erpnext/js/stock_analytics.js"); };
frappe.assets.views["Report"]();
frappe.require("assets/erpnext/js/stock_analytics.js");

View File

@ -10,7 +10,7 @@ frappe.pages['stock-ledger'].on_page_load = function(wrapper) {
new erpnext.StockLedger(wrapper); new erpnext.StockLedger(wrapper);
frappe.add_breadcrumbs("Stock") frappe.add_breadcrumbs("Stock")
} };
frappe.require("assets/erpnext/js/stock_grid_report.js"); frappe.require("assets/erpnext/js/stock_grid_report.js");

View File

@ -59,278 +59,256 @@ def delete_cancelled_entry(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabStock Ledger Entry` frappe.db.sql("""delete from `tabStock Ledger Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
def update_entries_after(args, allow_zero_rate=False, allow_negative_stock=False, verbose=1): class update_entries_after(object):
""" """
update valution rate and qty after transaction update valution rate and qty after transaction
from the current time-bucket onwards from the current time-bucket onwards
args = { :param args: args as dict
"item_code": "ABC",
"warehouse": "XYZ", args = {
"posting_date": "2012-12-12", "item_code": "ABC",
"posting_time": "12:00" "warehouse": "XYZ",
} "posting_date": "2012-12-12",
"posting_time": "12:00"
}
""" """
if not _exceptions: def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, verbose=1):
frappe.local.stockledger_exceptions = [] from frappe.model.meta import get_field_precision
if not allow_negative_stock: self.exceptions = []
allow_negative_stock = cint(frappe.db.get_default("allow_negative_stock")) self.verbose = verbose
self.allow_zero_rate = allow_zero_rate
self.allow_negative_stock = allow_negative_stock
previous_sle = get_sle_before_datetime(args) if self.allow_negative_stock==None:
self.allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings",
"allow_negative_stock"))
qty_after_transaction = flt(previous_sle.get("qty_after_transaction")) self.args = args
valuation_rate = flt(previous_sle.get("valuation_rate")) for key, value in args.iteritems():
stock_queue = json.loads(previous_sle.get("stock_queue") or "[]") setattr(self, key, value)
stock_value = flt(previous_sle.get("stock_value"))
prev_stock_value = flt(previous_sle.get("stock_value"))
entries_to_fix = get_sle_after_datetime(previous_sle or \ self.previous_sle = self.get_sle_before_datetime()
{"item_code": args["item_code"], "warehouse": args["warehouse"]}, for_update=True) self.previous_sle = self.previous_sle[0] if self.previous_sle else frappe._dict()
valuation_method = get_valuation_method(args["item_code"])
stock_value_difference = 0.0
for sle in entries_to_fix: for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
if sle.serial_no or not cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")): setattr(self, key, flt(self.previous_sle.get(key)))
self.company = frappe.db.get_value("Warehouse", self.warehouse, "company")
self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
currency=frappe.db.get_value("Company", self.company, "default_currency"))
self.prev_stock_value = self.stock_value
self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]")
self.valuation_method = get_valuation_method(self.item_code)
self.stock_value_difference = 0.0
self.build()
def build(self):
entries_to_fix = self.get_sle_after_datetime()
for sle in entries_to_fix:
self.process_sle(sle)
if self.exceptions:
self.raise_exceptions()
self.update_bin()
def update_bin(self):
# update bin
bin_name = frappe.db.get_value("Bin", {
"item_code": self.item_code,
"warehouse": self.warehouse
})
if not bin_name:
bin_doc = frappe.get_doc({
"doctype": "Bin",
"item_code": self.item_code,
"warehouse": self.warehouse
})
bin_doc.insert(ignore_permissions=True)
else:
bin_doc = frappe.get_doc("Bin", bin_name)
bin_doc.update({
"valuation_rate": self.valuation_rate,
"actual_qty": self.qty_after_transaction,
"stock_value": self.stock_value
})
bin_doc.save(ignore_permissions=True)
def process_sle(self, sle):
if sle.serial_no or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation # validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average # or when negative stock is not allowed for moving average
if not validate_negative_stock(qty_after_transaction, sle): if not self.validate_negative_stock(sle):
qty_after_transaction += flt(sle.actual_qty) self.qty_after_transaction += flt(sle.actual_qty)
continue return
if sle.serial_no: if sle.serial_no:
valuation_rate = get_serialized_values(qty_after_transaction, sle, valuation_rate) self.valuation_rate = self.get_serialized_values(sle)
qty_after_transaction += flt(sle.actual_qty) self.qty_after_transaction += flt(sle.actual_qty)
self.stock_value = self.qty_after_transaction * self.valuation_rate
else: else:
if sle.voucher_type=="Stock Reconciliation": if sle.voucher_type=="Stock Reconciliation":
valuation_rate = sle.valuation_rate # assert
qty_after_transaction = sle.qty_after_transaction self.valuation_rate = sle.valuation_rate
stock_queue = [[qty_after_transaction, valuation_rate]] self.qty_after_transaction = sle.qty_after_transaction
self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
self.stock_value = self.qty_after_transaction * self.valuation_rate
else: else:
if valuation_method == "Moving Average": if self.valuation_method == "Moving Average":
valuation_rate = get_moving_average_values(qty_after_transaction, sle, valuation_rate, allow_zero_rate) self.get_moving_average_values(sle)
self.qty_after_transaction += flt(sle.actual_qty)
self.stock_value = self.qty_after_transaction * self.valuation_rate
else: else:
valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue, allow_zero_rate) self.get_fifo_values(sle)
self.qty_after_transaction += flt(sle.actual_qty)
self.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
qty_after_transaction += flt(sle.actual_qty)
# get stock value
if sle.serial_no:
stock_value = qty_after_transaction * valuation_rate
elif valuation_method == "Moving Average":
stock_value = qty_after_transaction * valuation_rate
else:
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
# rounding as per precision # rounding as per precision
from frappe.model.meta import get_field_precision self.stock_value = flt(self.stock_value, self.precision)
meta = frappe.get_meta("Stock Ledger Entry")
stock_value = flt(stock_value, get_field_precision(meta.get_field("stock_value"), stock_value_difference = self.stock_value - self.prev_stock_value
frappe._dict({"fields": sle}))) self.prev_stock_value = self.stock_value
stock_value_difference = stock_value - prev_stock_value
prev_stock_value = stock_value
# update current sle # update current sle
frappe.db.sql("""update `tabStock Ledger Entry` sle.qty_after_transaction = self.qty_after_transaction
set qty_after_transaction=%s, valuation_rate=%s, stock_queue=%s, sle.valuation_rate = self.valuation_rate
stock_value=%s, stock_value_difference=%s where name=%s""", sle.stock_queue = json.dumps(self.stock_queue)
(qty_after_transaction, valuation_rate, sle.stock_value_difference = stock_value_difference
json.dumps(stock_queue), stock_value, stock_value_difference, sle.name)) sle.save()
if _exceptions: def validate_negative_stock(self, sle):
_raise_exceptions(args, verbose) """
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
diff = self.qty_after_transaction + flt(sle.actual_qty)
# update bin if diff < 0 and abs(diff) > 0.0001:
if not frappe.db.exists({"doctype": "Bin", "item_code": args["item_code"], # negative stock!
"warehouse": args["warehouse"]}): exc = sle.copy().update({"diff": diff})
bin_wrapper = frappe.get_doc({ self.exceptions.append(exc)
"doctype": "Bin", return False
"item_code": args["item_code"],
"warehouse": args["warehouse"],
})
bin_wrapper.flags.ignore_permissions = 1
bin_wrapper.insert()
frappe.db.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s,
stock_value=%s,
projected_qty = (actual_qty + indented_qty + ordered_qty + planned_qty - reserved_qty)
where item_code=%s and warehouse=%s""", (valuation_rate, qty_after_transaction,
stock_value, args["item_code"], args["warehouse"]))
def get_sle_before_datetime(args, for_update=False):
"""
get previous stock ledger entry before current time-bucket
Details:
get the last sle before the current time-bucket, so that all values
are reposted from the current time-bucket onwards.
this is necessary because at the time of cancellation, there may be
entries between the cancelled entries in the same time-bucket
"""
sle = get_stock_ledger_entries(args,
["timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)"],
"desc", "limit 1", for_update=for_update)
return sle and sle[0] or frappe._dict()
def get_sle_after_datetime(args, for_update=False):
"""get Stock Ledger Entries after a particular datetime, for reposting"""
# NOTE: using for update of
conditions = ["timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)"]
# Excluding name: Workaround for MariaDB timestamp() floating microsecond issue
if args.get("name"):
conditions.append("name!=%(name)s")
return get_stock_ledger_entries(args, conditions, "asc", for_update=for_update)
def get_stock_ledger_entries(args, conditions=None, order="desc", limit=None, for_update=False):
"""get stock ledger entries filtered by specific posting datetime conditions"""
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry`
where item_code = %%(item_code)s
and warehouse = %%(warehouse)s
and ifnull(is_cancelled, 'No')='No'
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, name %(order)s
%(limit)s %(for_update)s""" % {
"conditions": conditions and ("and " + " and ".join(conditions)) or "",
"limit": limit or "",
"for_update": for_update and "for update" or "",
"order": order
}, args, as_dict=1)
def validate_negative_stock(qty_after_transaction, sle):
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
diff = qty_after_transaction + flt(sle.actual_qty)
if not _exceptions:
frappe.local.stockledger_exceptions = []
if diff < 0 and abs(diff) > 0.0001:
# negative stock!
exc = sle.copy().update({"diff": diff})
_exceptions.append(exc)
return False
else:
return True
def get_serialized_values(qty_after_transaction, sle, valuation_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
serial_no = cstr(sle.serial_no).split("\n")
if incoming_rate < 0:
# wrong incoming rate
incoming_rate = valuation_rate
elif incoming_rate == 0 or flt(sle.actual_qty) < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
incoming_rate = flt(frappe.db.sql("""select avg(ifnull(purchase_rate, 0))
from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_no))),
tuple(serial_no))[0][0])
if incoming_rate and not valuation_rate:
valuation_rate = incoming_rate
else:
new_stock_qty = qty_after_transaction + actual_qty
if new_stock_qty > 0:
new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
if new_stock_value > 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
valuation_rate = new_stock_value / new_stock_qty
return valuation_rate
def get_moving_average_values(qty_after_transaction, sle, valuation_rate, allow_zero_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if flt(sle.actual_qty) > 0:
if qty_after_transaction < 0 and not valuation_rate:
# if negative stock, take current valuation rate as incoming rate
valuation_rate = incoming_rate
new_stock_qty = abs(qty_after_transaction) + actual_qty
new_stock_value = (abs(qty_after_transaction) * valuation_rate) + (actual_qty * incoming_rate)
if new_stock_qty:
valuation_rate = new_stock_value / flt(new_stock_qty)
elif not valuation_rate and qty_after_transaction <= 0:
valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, allow_zero_rate)
return abs(flt(valuation_rate))
def get_fifo_values(qty_after_transaction, sle, stock_queue, allow_zero_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if actual_qty > 0:
if not stock_queue:
stock_queue.append([0, 0])
if stock_queue[-1][0] > 0:
stock_queue.append([actual_qty, incoming_rate])
else: else:
qty = stock_queue[-1][0] + actual_qty return True
if qty == 0:
stock_queue.pop(-1) def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
serial_no = cstr(sle.serial_no).split("\n")
if incoming_rate < 0:
# wrong incoming rate
incoming_rate = self.valuation_rate
elif incoming_rate == 0 or flt(sle.actual_qty) < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
incoming_rate = flt(frappe.db.sql("""select avg(ifnull(purchase_rate, 0))
from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_no))),
tuple(serial_no))[0][0])
if incoming_rate and not self.valuation_rate:
self.valuation_rate = incoming_rate
else:
new_stock_qty = self.qty_after_transaction + actual_qty
if new_stock_qty > 0:
new_stock_value = self.qty_after_transaction * self.valuation_rate + actual_qty * incoming_rate
if new_stock_value > 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
self.valuation_rate = new_stock_value / new_stock_qty
def get_moving_average_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if flt(sle.actual_qty) > 0:
if self.qty_after_transaction < 0 and not self.valuation_rate:
# if negative stock, take current valuation rate as incoming rate
self.valuation_rate = incoming_rate
new_stock_qty = abs(self.qty_after_transaction) + actual_qty
new_stock_value = (abs(self.qty_after_transaction) * self.valuation_rate) + (actual_qty * incoming_rate)
if new_stock_qty:
self.valuation_rate = new_stock_value / flt(new_stock_qty)
elif not self.valuation_rate and self.qty_after_transaction <= 0:
valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.allow_zero_rate)
return abs(flt(valuation_rate))
def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if actual_qty > 0:
if not self.stock_queue:
self.stock_queue.append([0, 0])
if self.stock_queue[-1][0] > 0:
self.stock_queue.append([actual_qty, incoming_rate])
else: else:
stock_queue[-1] = [qty, incoming_rate] qty = self.stock_queue[-1][0] + actual_qty
else: if qty == 0:
qty_to_pop = abs(actual_qty) self.stock_queue.pop(-1)
while qty_to_pop: else:
if not stock_queue: self.stock_queue[-1] = [qty, incoming_rate]
stock_queue.append([0, get_valuation_rate(sle.item_code, sle.warehouse, allow_zero_rate) else:
if qty_after_transaction <= 0 else 0]) qty_to_pop = abs(actual_qty)
while qty_to_pop:
if not self.stock_queue:
if self.qty_after_transaction > 0:
_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.allow_zero_rate)
else:
_rate = 0
self.stock_queue.append([0, _rate])
batch = stock_queue[0] batch = self.stock_queue[0]
if qty_to_pop >= batch[0]: if qty_to_pop >= batch[0]:
# consume current batch # consume current batch
qty_to_pop = qty_to_pop - batch[0] qty_to_pop = qty_to_pop - batch[0]
stock_queue.pop(0) self.stock_queue.pop(0)
if not stock_queue and qty_to_pop: if not self.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn # stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch # negative stock, keep in as a negative batch
stock_queue.append([-qty_to_pop, batch[1]]) self.stock_queue.append([-qty_to_pop, batch[1]])
break break
else: else:
# qty found in current batch # qty found in current batch
# consume it and exit # consume it and exit
batch[0] = batch[0] - qty_to_pop batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0 qty_to_pop = 0
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue)) stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
stock_qty = sum((flt(batch[0]) for batch in stock_queue)) stock_qty = sum((flt(batch[0]) for batch in self.stock_queue))
valuation_rate = (stock_value / flt(stock_qty)) if stock_qty else 0 self.valuation_rate = (stock_value / flt(stock_qty)) if stock_qty else 0
return abs(valuation_rate) def get_sle_before_datetime(self):
"""get previous stock ledger entry before current time-bucket"""
return get_stock_ledger_entries(self.args, "<", "desc", "limit 1", for_update=False)
def _raise_exceptions(args, verbose=1): def get_sle_after_datetime(self):
deficiency = min(e["diff"] for e in _exceptions) """get Stock Ledger Entries after a particular datetime, for reposting"""
msg = _("Negative Stock Error ({6}) for Item {0} in Warehouse {1} on {2} {3} in {4} {5}").format(args["item_code"], return get_stock_ledger_entries(self.previous_sle or self.args, ">", "asc", for_update=True)
args.get("warehouse"), _exceptions[0]["posting_date"], _exceptions[0]["posting_time"],
_(_exceptions[0]["voucher_type"]), _exceptions[0]["voucher_no"], deficiency) def raise_exceptions(self):
if verbose: deficiency = min(e["diff"] for e in self.exceptions)
frappe.throw(msg, NegativeStockError) msg = _("Negative Stock Error ({6}) for Item {0} in Warehouse {1} on {2} {3} in {4} {5}").format(self.item_code,
else: self.warehouse, self.exceptions[0]["posting_date"], self.exceptions[0]["posting_time"],
raise NegativeStockError, msg _(self.exceptions[0]["voucher_type"]), self.exceptions[0]["voucher_no"], deficiency)
if self.verbose:
frappe.throw(msg, NegativeStockError)
else:
raise NegativeStockError, msg
def get_previous_sle(args, for_update=False): def get_previous_sle(args, for_update=False):
""" """
@ -346,13 +324,34 @@ def get_previous_sle(args, for_update=False):
"sle": "name of reference Stock Ledger Entry" "sle": "name of reference Stock Ledger Entry"
} }
""" """
if not args.get("sle"): args["sle"] = "" args["name"] = args.get("sle", None) or ""
sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
sle = get_stock_ledger_entries(args, ["name != %(sle)s",
"timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s)"],
"desc", "limit 1", for_update=for_update)
return sle and sle[0] or {} return sle and sle[0] or {}
def get_stock_ledger_entries(previous_sle, operator=None, order="desc", limit=None, for_update=False):
"""get stock ledger entries filtered by specific posting datetime conditions"""
conditions = "timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01"
if not previous_sle.get("posting_time"):
previous_sle["posting_time"] = "00:00"
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry`
where item_code = %%(item_code)s
and warehouse = %%(warehouse)s
and ifnull(is_cancelled, 'No')='No'
and %(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, name %(order)s
%(limit)s %(for_update)s""" % {
"conditions": conditions,
"limit": limit or "",
"for_update": for_update and "for update" or "",
"order": order
}, previous_sle, as_dict=1)
def get_valuation_rate(item_code, warehouse, allow_zero_rate=False): def get_valuation_rate(item_code, warehouse, allow_zero_rate=False):
last_valuation_rate = frappe.db.sql("""select valuation_rate last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` from `tabStock Ledger Entry`

View File

@ -34,19 +34,22 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
return sum(sle_map.values()) return sum(sle_map.values())
def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None): def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None, with_valuation_rate=False):
"""Returns stock balance quantity at given warehouse on given posting date or current date.
If `with_valuation_rate` is True, will return tuple (qty, rate)"""
if not posting_date: posting_date = nowdate() if not posting_date: posting_date = nowdate()
if not posting_time: posting_time = nowtime() if not posting_time: posting_time = nowtime()
last_entry = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry` last_entry = frappe.db.sql("""select qty_after_transaction, valuation_rate from `tabStock Ledger Entry`
where item_code=%s and warehouse=%s where item_code=%s and warehouse=%s
and timestamp(posting_date, posting_time) < timestamp(%s, %s) and timestamp(posting_date, posting_time) < timestamp(%s, %s)
order by timestamp(posting_date, posting_time) limit 1""", order by timestamp(posting_date, posting_time) limit 1""",
(item_code, warehouse, posting_date, posting_time)) (item_code, warehouse, posting_date, posting_time))
if last_entry: if with_valuation_rate:
return last_entry[0][0] return (last_entry[0][0], last_entry[0][1]) if last_entry else (0.0, 0.0)
else: else:
return 0.0 return last_entry[0][0] if last_entry else 0.0
def get_latest_stock_balance(): def get_latest_stock_balance():
bin_map = {} bin_map = {}

View File

@ -15,6 +15,8 @@ frappe.pages['support-analytics'].on_page_load = function(wrapper) {
} }
frappe.assets.views["Report"]();
erpnext.SupportAnalytics = frappe.views.GridReportWithPlot.extend({ erpnext.SupportAnalytics = frappe.views.GridReportWithPlot.extend({
init: function(wrapper) { init: function(wrapper) {
this._super({ this._super({

View File

@ -14,9 +14,9 @@
<br>{%= doc.item_name %}{% } %} <br>{%= doc.item_name %}{% } %}
{% if(doc.item_name != doc.description) { %} {% if(doc.item_name != doc.description) { %}
<p>{%= doc.description %}</p>{% } %} <p>{%= doc.description %}</p>{% } %}
<div> <p>
{% if(doc.sales_order || doc.against_sales_order) { %} {% if(doc.sales_order || doc.against_sales_order) { %}
<span class="label label-default" <span class="label label-default" style="margin-right: 10px;"
title="{%= __("Sales Order") %}"> title="{%= __("Sales Order") %}">
<i class="icon-file"></i> <i class="icon-file"></i>
{%= doc.sales_order || doc.against_sales_order %} {%= doc.sales_order || doc.against_sales_order %}
@ -36,22 +36,28 @@
} }
} }
%} %}
<span style="margin-left: 10px;" <span style="margin-right: 10px;"
title="{%= title %}"> title="{%= title %}">
<span class="label {%= label_class %}"> <span class="label {%= label_class %}">
{%= doc.warehouse %} {%= doc.warehouse %}
</span> </span>
</span> </span>
{% } %} {% } %}
</div> </p>
{% include "templates/form_grid/includes/visible_cols.html" %} {% include "templates/form_grid/includes/visible_cols.html" %}
{% if(doc.schedule_date) { %} {% if(doc.schedule_date) { %}
<div><span title="{%= __("Reqd By Date") %}" class="label {%= <div class="clearfix"></div>
(frappe.datetime.get_diff(doc.schedule_date) < 1 <div>
&& doc.received_qty < doc.qty) {% if(frappe.datetime.get_diff(doc.schedule_date) < 1 && doc.received_qty < doc.qty) { %}
? "label-danger" : "label-default" %}"> <span class="text-danger">
{%= doc.get_formatted("schedule_date") %}</span> {%= __("Overdue on {0}", [doc.get_formatted("schedule_date")]) %}
</div> </span>
{% } else { %}
<span class="text-muted">
{%= __("Due on {0}", [doc.get_formatted("schedule_date")]) %}
</span>
{% } %}
</div>
{% } %} {% } %}
</div> </div>

View File

@ -13,32 +13,32 @@
<br>{%= doc.item_name %}{% } %} <br>{%= doc.item_name %}{% } %}
{% if(doc.item_name != doc.description) { %} {% if(doc.item_name != doc.description) { %}
<p>{%= doc.description %}</p>{% } %} <p>{%= doc.description %}</p>{% } %}
{% include "templates/form_grid/includes/visible_cols.html" %} <div>
{% if(doc.warehouse) { %}
<span class="label label-default" title="{%= __("For Warehouse") %}"
style="margin-right: 10px;">
{%= doc.warehouse %}
</span>
{% } %}
{% if(doc.schedule_date) { %} {% if(doc.schedule_date) { %}
<br><span title="{%= __("Reqd By Date") %}" class="label {%= <span title="{%= __("Reqd By Date") %}" class="label {%=
(frappe.datetime.get_diff(doc.schedule_date, frappe.datetime.get_today()) < 0 (frappe.datetime.get_diff(doc.schedule_date, frappe.datetime.get_today()) < 0
&& doc.ordered_qty < doc.qty) && doc.ordered_qty < doc.qty)
? "label-danger" : "label-default" %}"> ? "label-danger" : "label-default" %}">
{%= doc.get_formatted("schedule_date") %}</span> {%= doc.get_formatted("schedule_date") %}</span>
{% } %} {% } %}
</div>
{% include "templates/form_grid/includes/visible_cols.html" %}
</div> </div>
<!-- qty --> <!-- qty -->
<div class="col-sm-3 text-right"> <div class="col-sm-3 text-right">
{%= doc.get_formatted("qty") %} {%= doc.get_formatted("qty") %}
<small>{%= doc.uom || doc.stock_uom %}</small> <div class="small">{%= doc.uom || doc.stock_uom %}</div>
{% var completed = {% if(doc.qty == doc.ordered_qty) { %}
100 - cint((doc.qty - cint(doc.ordered_qty)) * 100 / doc.qty), <div class="small text-success">{%= __("Ordered") %}</div>
title = __("Ordered"); %} {% } else { %}
{% include "templates/form_grid/includes/progress.html" %} <div class="small text-muted">{%= __("{0} Ordered", doc.ordered_qty) %}</div>
{% if(doc.warehouse) { %}
<div style="overflow:hidden; min-height: 25px;
white-space:nowrap; text-overflow: ellipsis;"
title="{%= __("For Warehouse") %}">
<span class="label label-default">
{%= doc.warehouse %}
</span>
</div>
{% } %} {% } %}
</div> </div>
</div> </div>

View File

@ -18,15 +18,18 @@
<p>{%= doc.description %}</p>{% } %} <p>{%= doc.description %}</p>{% } %}
{% include "templates/form_grid/includes/visible_cols.html" %} {% include "templates/form_grid/includes/visible_cols.html" %}
<div> <div>
{% if(doc.s_warehouse) { %}<span class="label {% if(doc.s_warehouse) { %}
{%= (doc.actual_qty >= doc.qty) ? "label-success" <span class="label label-primary">
: "label-danger" %}" {%= doc.s_warehouse || "" %}</span>
title="{%= (doc.actual_qty >= doc.qty) ? __("In Stock") {% } %}
: __("Not In Stock") %}"> <i class="octicon octicon-arrow-small-right"></i>
{%= doc.s_warehouse || "" %}</span>{% } %}
<i class="icon-long-arrow-right"></i>
{% if(doc.t_warehouse) { %}<span class="label label-primary"> {% if(doc.t_warehouse) { %}<span class="label label-primary">
{%= doc.t_warehouse || "" %}</span>{% } %} {%= doc.t_warehouse || "" %}</span>{% } %}
{% if(doc.s_warehouse && doc.actual_qty < doc.qty) { %}
<span class="text-danger small" style="margin-left: 15px;">
<span class="octicon octicon-stop" style="font-size: 12px;"></span> Not in Stock
</span>
{% } %}
</div> </div>
</div> </div>