mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 00:14:50 +00:00
Merge branch 'master' of github.com:webnotes/erpnext
This commit is contained in:
@@ -1,264 +0,0 @@
|
|||||||
# ERPNext - web based ERP (http://erpnext.com)
|
|
||||||
# Copyright (C) 2012 Web Notes Technologies Pvt Ltd
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import webnotes
|
|
||||||
from webnotes.utils import cstr, flt, get_defaults, nowdate, formatdate
|
|
||||||
from webnotes import msgprint
|
|
||||||
from webnotes.model.code import get_obj
|
|
||||||
sql = webnotes.conn.sql
|
|
||||||
|
|
||||||
|
|
||||||
class DocType:
|
|
||||||
def __init__(self, doc, doclist=[]):
|
|
||||||
self.doc = doc
|
|
||||||
self.doclist = doclist
|
|
||||||
self.validated = 1
|
|
||||||
self.data = []
|
|
||||||
self.val_method = get_defaults()['valuation_method']
|
|
||||||
|
|
||||||
def get_template(self):
|
|
||||||
if self.val_method == 'Moving Average':
|
|
||||||
return [['Item Code', 'Warehouse', 'Quantity', 'Valuation Rate']]
|
|
||||||
else:
|
|
||||||
return [['Item Code', 'Warehouse', 'Quantity', 'Incoming Rate']]
|
|
||||||
|
|
||||||
|
|
||||||
def read_csv_content(self, submit = 1):
|
|
||||||
"""Get csv data"""
|
|
||||||
if submit:
|
|
||||||
from webnotes.utils.datautils import read_csv_content_from_attached_file
|
|
||||||
data = read_csv_content_from_attached_file(self.doc)
|
|
||||||
else:
|
|
||||||
from webnotes.utils.datautils import read_csv_content
|
|
||||||
data = read_csv_content(self.doc.diff_info)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def convert_into_list(self, data, submit=1):
|
|
||||||
"""Convert csv data into list"""
|
|
||||||
count = 1
|
|
||||||
for s in data:
|
|
||||||
count += 1
|
|
||||||
if count == 2 and submit:
|
|
||||||
if cstr(s[0]).strip() != 'Item Code' or cstr(s[1]).strip() != 'Warehouse':
|
|
||||||
msgprint("First row of the attachment always should be same as \
|
|
||||||
template(Item Code, Warehouse, Quantity \
|
|
||||||
and Valuation Rate/Incoming Rate)", raise_exception=1)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
# validate
|
|
||||||
if (submit and len(s) != 4) or (not submit and len(s) != 6):
|
|
||||||
msgprint("Data entered at Row No " + cstr(count) + " in Attachment File is not in correct format.", raise_exception=1)
|
|
||||||
self.validated = 0
|
|
||||||
self.validate_item(s[0], count)
|
|
||||||
self.validate_warehouse(s[1], count)
|
|
||||||
|
|
||||||
self.data.append(s)
|
|
||||||
|
|
||||||
if not self.validated:
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
|
|
||||||
def get_reconciliation_data(self, submit = 1):
|
|
||||||
"""Read and validate csv data"""
|
|
||||||
data = self.read_csv_content(submit)
|
|
||||||
self.convert_into_list(data, submit)
|
|
||||||
|
|
||||||
def validate_item(self, item, count):
|
|
||||||
""" Validate item exists and non-serialized"""
|
|
||||||
det = sql("select item_code, has_serial_no from `tabItem` where name = %s", cstr(item), as_dict = 1)
|
|
||||||
if not det:
|
|
||||||
msgprint("Item: " + cstr(item) + " mentioned at Row No. " + cstr(count) + "does not exist in the system")
|
|
||||||
self.validated = 0
|
|
||||||
elif det and det[0]['has_serial_no'] == 'Yes':
|
|
||||||
msgprint("""You cannot make Stock Reconciliation of items having serial no. \n
|
|
||||||
You can directly upload serial no to update their inventory. \n
|
|
||||||
Please remove Item Code : %s at Row No. %s""" %(cstr(item), cstr(count)))
|
|
||||||
self.validated = 0
|
|
||||||
|
|
||||||
|
|
||||||
def validate_warehouse(self, wh, count,):
|
|
||||||
"""Validate warehouse exists"""
|
|
||||||
if not sql("select name from `tabWarehouse` where name = %s", cstr(wh)):
|
|
||||||
msgprint("Warehouse: " + cstr(wh) + " mentioned at Row No. " + cstr(count) + " does not exist in the system")
|
|
||||||
self.validated = 0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
"""Validate attachment data"""
|
|
||||||
if self.doc.file_list:
|
|
||||||
self.get_reconciliation_data()
|
|
||||||
|
|
||||||
def get_system_stock(self, it, wh):
|
|
||||||
"""get actual qty on reconciliation date and time as per system"""
|
|
||||||
bin = sql("select name from tabBin where item_code=%s and warehouse=%s", (it, wh))
|
|
||||||
prev_sle = bin and get_obj('Bin', bin[0][0]).get_prev_sle(self.doc.reconciliation_date, self.doc.reconciliation_time) or {}
|
|
||||||
return {
|
|
||||||
'actual_qty': prev_sle.get('qty_after_transaction', 0),
|
|
||||||
'stock_uom' : sql("select stock_uom from tabItem where name = %s", it)[0][0],
|
|
||||||
'val_rate' : prev_sle.get('valuation_rate', 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_incoming_rate(self, row, qty_diff, sys_stock):
|
|
||||||
"""Calculate incoming rate to maintain valuation rate"""
|
|
||||||
if qty_diff:
|
|
||||||
if self.val_method == 'Moving Average':
|
|
||||||
in_rate = flt(row[3]) + (flt(sys_stock['actual_qty'])*(flt(row[3]) - flt(sys_stock['val_rate'])))/ flt(qty_diff)
|
|
||||||
elif not sys_stock and not row[3]:
|
|
||||||
msgprint("Incoming Rate is mandatory for item: %s and warehouse: %s" % (rpw[0], row[1]), raise_exception=1)
|
|
||||||
else:
|
|
||||||
in_rate = qty_diff > 0 and row[3] or 0
|
|
||||||
else:
|
|
||||||
in_rate = 0
|
|
||||||
|
|
||||||
return in_rate
|
|
||||||
|
|
||||||
def make_sl_entry(self, row, qty_diff, sys_stock):
|
|
||||||
"""Make stock ledger entry"""
|
|
||||||
in_rate = self.get_incoming_rate(row, qty_diff, sys_stock)
|
|
||||||
values = [{
|
|
||||||
'item_code' : row[0],
|
|
||||||
'warehouse' : row[1],
|
|
||||||
'transaction_date' : nowdate(),
|
|
||||||
'posting_date' : self.doc.reconciliation_date,
|
|
||||||
'posting_time' : self.doc.reconciliation_time,
|
|
||||||
'voucher_type' : self.doc.doctype,
|
|
||||||
'voucher_no' : self.doc.name,
|
|
||||||
'voucher_detail_no' : self.doc.name,
|
|
||||||
'actual_qty' : flt(qty_diff),
|
|
||||||
'stock_uom' : sys_stock['stock_uom'],
|
|
||||||
'incoming_rate' : in_rate,
|
|
||||||
'company' : get_defaults()['company'],
|
|
||||||
'fiscal_year' : get_defaults()['fiscal_year'],
|
|
||||||
'is_cancelled' : 'No',
|
|
||||||
'batch_no' : '',
|
|
||||||
'serial_no' : ''
|
|
||||||
}]
|
|
||||||
get_obj('Stock Ledger', 'Stock Ledger').update_stock(values)
|
|
||||||
|
|
||||||
def make_entry_for_valuation(self, row, sys_stock):
|
|
||||||
self.make_sl_entry(row, 1, sys_stock)
|
|
||||||
sys_stock['val_rate'] = row[3]
|
|
||||||
sys_stock['actual_qty'] += 1
|
|
||||||
self.make_sl_entry(row, -1, sys_stock)
|
|
||||||
|
|
||||||
def do_stock_reco(self):
|
|
||||||
"""
|
|
||||||
Make stock entry of qty diff, calculate incoming rate to maintain valuation rate.
|
|
||||||
If no qty diff, but diff in valuation rate, make (+1,-1) entry to update valuation
|
|
||||||
"""
|
|
||||||
self.diff_info = ''
|
|
||||||
for row in self.data:
|
|
||||||
# Get qty as per system
|
|
||||||
sys_stock = self.get_system_stock(row[0],row[1])
|
|
||||||
|
|
||||||
# Diff between file and system
|
|
||||||
qty_diff = row[2] != '~' and flt(row[2]) - flt(sys_stock['actual_qty']) or 0
|
|
||||||
rate_diff = row[3] != '~' and flt(row[3]) - flt(sys_stock['val_rate']) or 0
|
|
||||||
|
|
||||||
# Make sl entry
|
|
||||||
if qty_diff:
|
|
||||||
self.make_sl_entry(row, qty_diff, sys_stock)
|
|
||||||
sys_stock['actual_qty'] += qty_diff
|
|
||||||
|
|
||||||
|
|
||||||
if (not qty_diff and rate_diff) or qty_diff < 0 and self.val_method == 'Moving Average':
|
|
||||||
self.make_entry_for_valuation(row, sys_stock)
|
|
||||||
|
|
||||||
|
|
||||||
r = [cstr(i) for i in row] + [cstr(qty_diff), cstr(rate_diff)]
|
|
||||||
self.store_diff_info(r)
|
|
||||||
|
|
||||||
msgprint("Stock Reconciliation Completed Successfully...")
|
|
||||||
|
|
||||||
def store_diff_info(self, r):
|
|
||||||
"""Add diffs column in attached file"""
|
|
||||||
|
|
||||||
# add header
|
|
||||||
if not self.diff_info:
|
|
||||||
if self.val_method == 'Moving Average':
|
|
||||||
self.diff_info += "Item Code, Warehouse, Qty, Valuation Rate, Qty Diff, Rate Diff"
|
|
||||||
else:
|
|
||||||
self.diff_info += "Item Code, Warehouse, Qty, Incoming Rate, Qty Diff, Rate Diff"
|
|
||||||
|
|
||||||
|
|
||||||
# add data
|
|
||||||
self.diff_info += "\n" + ','.join(r)
|
|
||||||
|
|
||||||
webnotes.conn.set(self.doc, 'diff_info', self.diff_info)
|
|
||||||
|
|
||||||
|
|
||||||
def on_submit(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.doc.file_list:
|
|
||||||
msgprint("Please attach file before submitting.", raise_exception=1)
|
|
||||||
else:
|
|
||||||
self.do_stock_reco()
|
|
||||||
|
|
||||||
|
|
||||||
def on_cancel(self):
|
|
||||||
self.cancel_stock_ledger_entries()
|
|
||||||
self.update_entries_after()
|
|
||||||
|
|
||||||
def cancel_stock_ledger_entries(self):
|
|
||||||
webnotes.conn.sql("""
|
|
||||||
update `tabStock Ledger Entry`
|
|
||||||
set is_cancelled = 'Yes'
|
|
||||||
where voucher_type = 'Stock Reconciliation' and voucher_no = %s
|
|
||||||
""", self.doc.name)
|
|
||||||
|
|
||||||
def update_entries_after(self):
|
|
||||||
# get distinct combination of item_code and warehouse to update bin
|
|
||||||
item_warehouse = webnotes.conn.sql("""select distinct item_code, warehouse
|
|
||||||
from `tabStock Ledger Entry` where voucher_no = %s and is_cancelled = 'Yes'
|
|
||||||
and voucher_type = 'Stock Reconciliation'""", self.doc.name)
|
|
||||||
|
|
||||||
from webnotes.model.code import get_obj
|
|
||||||
errors = []
|
|
||||||
for d in item_warehouse:
|
|
||||||
bin = webnotes.conn.sql("select name from `tabBin` where item_code = %s and \
|
|
||||||
warehouse = %s", (d[0], d[1]))
|
|
||||||
try:
|
|
||||||
get_obj('Bin',
|
|
||||||
bin[0][0]).update_entries_after(self.doc.reconciliation_date,
|
|
||||||
self.doc.reconciliation_time, verbose=0)
|
|
||||||
except webnotes.ValidationError, e:
|
|
||||||
errors.append([d[0], d[1], e])
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
import re
|
|
||||||
error_msg = [["Item Code", "Warehouse", "Qty"]]
|
|
||||||
qty_regex = re.compile(": <b>(.*)</b>")
|
|
||||||
for e in errors:
|
|
||||||
qty = qty_regex.findall(unicode(e[2]))
|
|
||||||
qty = qty and abs(flt(qty[0])) or None
|
|
||||||
|
|
||||||
error_msg.append([e[0], e[1], flt(qty)])
|
|
||||||
|
|
||||||
webnotes.msgprint("""Your stock is going into negative value \
|
|
||||||
in a future transaction.
|
|
||||||
To cancel, you need to create a stock entry with the \
|
|
||||||
following values on %s %s""" % \
|
|
||||||
(formatdate(self.doc.reconciliation_date), self.doc.reconciliation_time))
|
|
||||||
webnotes.msgprint(error_msg, as_table=1, raise_exception=1)
|
|
||||||
|
|
||||||
@webnotes.whitelist()
|
|
||||||
def upload():
|
|
||||||
from webnotes.utils.datautils import read_csv_content_from_uploaded_file
|
|
||||||
return read_csv_content_from_uploaded_file()
|
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
{
|
{
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"creation": "2013-01-11 12:04:17",
|
"creation": "2013-01-14 15:14:40",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"modified": "2013-01-11 15:36:21"
|
"modified": "2013-01-15 12:25:13"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_attach": 0,
|
"allow_attach": 0,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"allow_print": 1,
|
"allow_print": 1,
|
||||||
"search_fields": "reconciliation_date",
|
"search_fields": "posting_date",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"autoname": "SR/.######",
|
"autoname": "SR/.######",
|
||||||
@@ -31,15 +31,16 @@
|
|||||||
{
|
{
|
||||||
"parent": "Stock Reconciliation",
|
"parent": "Stock Reconciliation",
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"doctype": "DocPerm",
|
|
||||||
"cancel": 1,
|
"cancel": 1,
|
||||||
"name": "__common__",
|
"name": "__common__",
|
||||||
"amend": 1,
|
"amend": 0,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
|
"doctype": "DocPerm",
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1,
|
"write": 1,
|
||||||
"parenttype": "DocType",
|
"parenttype": "DocType",
|
||||||
"role": "Material Manager",
|
"role": "Material Manager",
|
||||||
|
"report": 1,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"parentfield": "permissions"
|
"parentfield": "permissions"
|
||||||
},
|
},
|
||||||
@@ -115,7 +116,7 @@
|
|||||||
"doctype": "DocField",
|
"doctype": "DocField",
|
||||||
"label": "Reconciliation JSON",
|
"label": "Reconciliation JSON",
|
||||||
"fieldname": "reconciliation_json",
|
"fieldname": "reconciliation_json",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Long Text",
|
||||||
"hidden": 1
|
"hidden": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ erpnext.StockLedger = erpnext.StockGridReport.extend({
|
|||||||
|
|
||||||
// initialize warehouse-item map
|
// initialize warehouse-item map
|
||||||
this.item_warehouse = {};
|
this.item_warehouse = {};
|
||||||
|
var from_datetime = dateutil.str_to_obj(me.from_date + " 00:00:00");
|
||||||
|
var to_datetime = dateutil.str_to_obj(me.to_date + " 23:59:59");
|
||||||
|
|
||||||
//
|
//
|
||||||
for(var i=0, j=data.length; i<j; i++) {
|
for(var i=0, j=data.length; i<j; i++) {
|
||||||
@@ -162,7 +164,7 @@ erpnext.StockLedger = erpnext.StockGridReport.extend({
|
|||||||
var item = me.item_by_name[sl.item_code]
|
var item = me.item_by_name[sl.item_code]
|
||||||
var wh = me.get_item_warehouse(sl.warehouse, sl.item_code);
|
var wh = me.get_item_warehouse(sl.warehouse, sl.item_code);
|
||||||
sl.description = item.description;
|
sl.description = item.description;
|
||||||
sl.posting_datetime = sl.posting_date + " " + sl.posting_time;
|
sl.posting_datetime = sl.posting_date + " " + (sl.posting_time || "00:00:00");
|
||||||
sl.brand = item.brand;
|
sl.brand = item.brand;
|
||||||
var posting_datetime = dateutil.str_to_obj(sl.posting_datetime);
|
var posting_datetime = dateutil.str_to_obj(sl.posting_datetime);
|
||||||
|
|
||||||
@@ -171,11 +173,11 @@ erpnext.StockLedger = erpnext.StockGridReport.extend({
|
|||||||
var value_diff = me.get_value_diff(wh, sl, is_fifo);
|
var value_diff = me.get_value_diff(wh, sl, is_fifo);
|
||||||
|
|
||||||
// opening, transactions, closing, total in, total out
|
// opening, transactions, closing, total in, total out
|
||||||
var before_end = posting_datetime <= dateutil.str_to_obj(me.to_date + " 23:59:59");
|
var before_end = posting_datetime <= to_datetime;
|
||||||
if((!me.is_default("item_code") ? me.apply_filter(sl, "item_code") : true)
|
if((!me.is_default("item_code") ? me.apply_filter(sl, "item_code") : true)
|
||||||
&& me.apply_filter(sl, "warehouse") && me.apply_filter(sl, "voucher_no")
|
&& me.apply_filter(sl, "warehouse") && me.apply_filter(sl, "voucher_no")
|
||||||
&& me.apply_filter(sl, "brand")) {
|
&& me.apply_filter(sl, "brand")) {
|
||||||
if(posting_datetime < dateutil.str_to_obj(me.from_date)) {
|
if(posting_datetime < from_datetime) {
|
||||||
opening.balance += sl.qty;
|
opening.balance += sl.qty;
|
||||||
opening.balance_value += value_diff;
|
opening.balance_value += value_diff;
|
||||||
} else if(before_end) {
|
} else if(before_end) {
|
||||||
|
|||||||
Reference in New Issue
Block a user