mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 18:59:08 +00:00
Merge branch 'hotfix' of https://github.com/frappe/erpnext into quotation-fix
This commit is contained in:
@@ -21,11 +21,39 @@ class BankAccount(Document):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_company()
|
self.validate_company()
|
||||||
|
self.validate_iban()
|
||||||
|
|
||||||
def validate_company(self):
|
def validate_company(self):
|
||||||
if self.is_company_account and not self.company:
|
if self.is_company_account and not self.company:
|
||||||
frappe.throw(_("Company is manadatory for company account"))
|
frappe.throw(_("Company is manadatory for company account"))
|
||||||
|
|
||||||
|
def validate_iban(self):
|
||||||
|
'''
|
||||||
|
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||||
|
'''
|
||||||
|
# IBAN field is optional
|
||||||
|
if not self.iban:
|
||||||
|
return
|
||||||
|
|
||||||
|
def encode_char(c):
|
||||||
|
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||||
|
return str(9 + ord(c) - 64)
|
||||||
|
|
||||||
|
# remove whitespaces, upper case to get the right number from ord()
|
||||||
|
iban = ''.join(self.iban.split(' ')).upper()
|
||||||
|
|
||||||
|
# Move country code and checksum from the start to the end
|
||||||
|
flipped = iban[4:] + iban[:4]
|
||||||
|
|
||||||
|
# Encode characters as numbers
|
||||||
|
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||||
|
|
||||||
|
to_check = int(''.join(encoded))
|
||||||
|
|
||||||
|
if to_check % 97 != 1:
|
||||||
|
frappe.throw(_('IBAN is not valid'))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_bank_account(doctype, docname):
|
def make_bank_account(doctype, docname):
|
||||||
doc = frappe.new_doc("Bank Account")
|
doc = frappe.new_doc("Bank Account")
|
||||||
|
|||||||
@@ -4,9 +4,46 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe import ValidationError
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Bank Account')
|
# test_records = frappe.get_test_records('Bank Account')
|
||||||
|
|
||||||
class TestBankAccount(unittest.TestCase):
|
class TestBankAccount(unittest.TestCase):
|
||||||
pass
|
|
||||||
|
def test_validate_iban(self):
|
||||||
|
valid_ibans = [
|
||||||
|
'GB82 WEST 1234 5698 7654 32',
|
||||||
|
'DE91 1000 0000 0123 4567 89',
|
||||||
|
'FR76 3000 6000 0112 3456 7890 189'
|
||||||
|
]
|
||||||
|
|
||||||
|
invalid_ibans = [
|
||||||
|
# wrong checksum (3rd place)
|
||||||
|
'GB72 WEST 1234 5698 7654 32',
|
||||||
|
'DE81 1000 0000 0123 4567 89',
|
||||||
|
'FR66 3000 6000 0112 3456 7890 189'
|
||||||
|
]
|
||||||
|
|
||||||
|
bank_account = frappe.get_doc({'doctype':'Bank Account'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
bank_account.validate_iban()
|
||||||
|
except AttributeError:
|
||||||
|
msg = _('BankAccount.validate_iban() failed for empty IBAN')
|
||||||
|
self.fail(msg=msg)
|
||||||
|
|
||||||
|
for iban in valid_ibans:
|
||||||
|
bank_account.iban = iban
|
||||||
|
try:
|
||||||
|
bank_account.validate_iban()
|
||||||
|
except ValidationError:
|
||||||
|
msg = _('BankAccount.validate_iban() failed for valid IBAN {}'.format(iban))
|
||||||
|
self.fail(msg=msg)
|
||||||
|
|
||||||
|
for not_iban in invalid_ibans:
|
||||||
|
bank_account.iban = not_iban
|
||||||
|
msg = _('BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban))
|
||||||
|
with self.assertRaises(ValidationError, msg=msg):
|
||||||
|
bank_account.validate_iban()
|
||||||
|
|||||||
@@ -296,6 +296,12 @@ frappe.ui.form.on('Asset', {
|
|||||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
gross_purchase_amount: function(frm) {
|
||||||
|
frm.doc.finance_books.forEach(d => {
|
||||||
|
frm.events.set_depreciation_rate(frm, d);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
set_depreciation_rate: function(frm, row) {
|
set_depreciation_rate: function(frm, row) {
|
||||||
if (row.total_number_of_depreciations && row.frequency_of_depreciation) {
|
if (row.total_number_of_depreciations && row.frequency_of_depreciation) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
def set_depreciation_rate(self):
|
def set_depreciation_rate(self):
|
||||||
for d in self.get("finance_books"):
|
for d in self.get("finance_books"):
|
||||||
d.rate_of_depreciation = self.get_depreciation_rate(d)
|
d.rate_of_depreciation = self.get_depreciation_rate(d, on_validate=True)
|
||||||
|
|
||||||
def make_depreciation_schedule(self):
|
def make_depreciation_schedule(self):
|
||||||
depreciation_method = [d.depreciation_method for d in self.finance_books]
|
depreciation_method = [d.depreciation_method for d in self.finance_books]
|
||||||
@@ -125,7 +125,7 @@ class Asset(AccountsController):
|
|||||||
no_of_depreciations * cint(d.frequency_of_depreciation))
|
no_of_depreciations * cint(d.frequency_of_depreciation))
|
||||||
|
|
||||||
total_days = date_diff(end_date, self.available_for_use_date)
|
total_days = date_diff(end_date, self.available_for_use_date)
|
||||||
rate_per_day = value_after_depreciation / total_days
|
rate_per_day = (value_after_depreciation - d.get("expected_value_after_useful_life")) / total_days
|
||||||
|
|
||||||
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
|
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
|
||||||
cint(self.number_of_depreciations_booked)
|
cint(self.number_of_depreciations_booked)
|
||||||
@@ -291,8 +291,8 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
def validate_expected_value_after_useful_life(self):
|
def validate_expected_value_after_useful_life(self):
|
||||||
for row in self.get('finance_books'):
|
for row in self.get('finance_books'):
|
||||||
accumulated_depreciation_after_full_schedule = \
|
accumulated_depreciation_after_full_schedule = max([d.accumulated_depreciation_amount
|
||||||
max([d.accumulated_depreciation_amount for d in self.get("schedules") if d.finance_book_id == row.idx])
|
for d in self.get("schedules") if cint(d.finance_book_id) == row.idx])
|
||||||
|
|
||||||
asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
|
asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) -
|
||||||
flt(accumulated_depreciation_after_full_schedule),
|
flt(accumulated_depreciation_after_full_schedule),
|
||||||
@@ -403,7 +403,7 @@ class Asset(AccountsController):
|
|||||||
make_gl_entries(gl_entries)
|
make_gl_entries(gl_entries)
|
||||||
self.db_set('booked_fixed_asset', 1)
|
self.db_set('booked_fixed_asset', 1)
|
||||||
|
|
||||||
def get_depreciation_rate(self, args):
|
def get_depreciation_rate(self, args, on_validate=False):
|
||||||
if isinstance(args, string_types):
|
if isinstance(args, string_types):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
@@ -420,7 +420,10 @@ class Asset(AccountsController):
|
|||||||
if args.get("depreciation_method") == 'Double Declining Balance':
|
if args.get("depreciation_method") == 'Double Declining Balance':
|
||||||
return 200.0 / args.get("total_number_of_depreciations")
|
return 200.0 / args.get("total_number_of_depreciations")
|
||||||
|
|
||||||
if args.get("depreciation_method") == "Written Down Value" and not args.get("rate_of_depreciation"):
|
if args.get("depreciation_method") == "Written Down Value":
|
||||||
|
if args.get("rate_of_depreciation") and on_validate:
|
||||||
|
return args.get("rate_of_depreciation")
|
||||||
|
|
||||||
no_of_years = flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation"))) / 12
|
no_of_years = flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation"))) / 12
|
||||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||||
|
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ class TestAsset(unittest.TestCase):
|
|||||||
asset.save()
|
asset.save()
|
||||||
self.assertEqual(asset.status, "Draft")
|
self.assertEqual(asset.status, "Draft")
|
||||||
expected_schedules = [
|
expected_schedules = [
|
||||||
["2020-06-06", 163.93, 163.93],
|
["2020-06-06", 147.54, 147.54],
|
||||||
["2021-04-06", 49836.07, 50000.0],
|
["2021-04-06", 44852.46, 45000.0],
|
||||||
["2022-02-06", 40000.0, 90000.00]
|
["2022-02-06", 45000.0, 90000.00]
|
||||||
]
|
]
|
||||||
|
|
||||||
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||||
@@ -130,8 +130,8 @@ class TestAsset(unittest.TestCase):
|
|||||||
self.assertEqual(asset.status, "Draft")
|
self.assertEqual(asset.status, "Draft")
|
||||||
asset.save()
|
asset.save()
|
||||||
expected_schedules = [
|
expected_schedules = [
|
||||||
["2020-06-06", 197.37, 40197.37],
|
["2020-06-06", 164.47, 40164.47],
|
||||||
["2021-04-06", 49802.63, 90000.00]
|
["2021-04-06", 49835.53, 90000.00]
|
||||||
]
|
]
|
||||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||||
for d in asset.get("schedules")]
|
for d in asset.get("schedules")]
|
||||||
@@ -266,8 +266,8 @@ class TestAsset(unittest.TestCase):
|
|||||||
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR")
|
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR")
|
||||||
|
|
||||||
expected_gle = (
|
expected_gle = (
|
||||||
("_Test Accumulated Depreciations - _TC", 0.0, 35699.15),
|
("_Test Accumulated Depreciations - _TC", 0.0, 32129.24),
|
||||||
("_Test Depreciations - _TC", 35699.15, 0.0)
|
("_Test Depreciations - _TC", 32129.24, 0.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result !== "disabled") {
|
if (result !== "disabled") {
|
||||||
|
if (result.plaid_env == undefined || result.plaid_public_key == undefined) {
|
||||||
|
frappe.throw(__("Please add valid Plaid api keys in site_config.json first"));
|
||||||
|
}
|
||||||
me.plaid_env = result.plaid_env;
|
me.plaid_env = result.plaid_env;
|
||||||
me.plaid_public_key = result.plaid_public_key;
|
me.plaid_public_key = result.plaid_public_key;
|
||||||
me.client_name = result.client_name;
|
me.client_name = result.client_name;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
'target_parent_dt': 'Purchase Order',
|
'target_parent_dt': 'Purchase Order',
|
||||||
'target_parent_field': 'per_received',
|
'target_parent_field': 'per_received',
|
||||||
'target_ref_field': 'qty',
|
'target_ref_field': 'qty',
|
||||||
'source_field': 'qty',
|
'source_field': 'received_qty',
|
||||||
'percent_join_field': 'purchase_order',
|
'percent_join_field': 'purchase_order',
|
||||||
'overflow_type': 'receipt'
|
'overflow_type': 'receipt'
|
||||||
},
|
},
|
||||||
|
|||||||
0
erpnext/stock/report/inactive_items/__init__.py
Normal file
0
erpnext/stock/report/inactive_items/__init__.py
Normal file
34
erpnext/stock/report/inactive_items/inactive_items.js
Normal file
34
erpnext/stock/report/inactive_items/inactive_items.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Inactive Items"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "item",
|
||||||
|
label: __("Item"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "item_group",
|
||||||
|
label: __("Item Group"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item Group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "based_on",
|
||||||
|
label: __("Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Sales Order\nSales Invoice",
|
||||||
|
default: "Sales Order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "days",
|
||||||
|
label: __("Days Since Last order"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: [30, 60, 90],
|
||||||
|
default: 30
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
31
erpnext/stock/report/inactive_items/inactive_items.json
Normal file
31
erpnext/stock/report/inactive_items/inactive_items.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"creation": "2019-04-16 16:05:00.647308",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"letter_head": "Test Letter Head 1",
|
||||||
|
"modified": "2019-04-16 16:06:33.630043",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
|
"name": "Inactive Items",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Sales Invoice",
|
||||||
|
"report_name": "Inactive Items",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Accounts User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Accounts Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Auditor"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
148
erpnext/stock/report/inactive_items/inactive_items.py
Normal file
148
erpnext/stock/report/inactive_items/inactive_items.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import getdate, add_days, today, cint
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
def get_columns():
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
"fieldname": "territory",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Territory"),
|
||||||
|
"options": "Territory",
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Item Group"),
|
||||||
|
"options": "Item Group",
|
||||||
|
"width": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Item",
|
||||||
|
"label": "Item",
|
||||||
|
"width": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Item Name"),
|
||||||
|
"width": 150
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Customer"),
|
||||||
|
"options": "Customer",
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "last_order_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": _("Last Order Date"),
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("Quantity"),
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "days_since_last_order",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": _("Days Since Last Order"),
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(filters):
|
||||||
|
|
||||||
|
data = []
|
||||||
|
items = get_items(filters)
|
||||||
|
sales_invoice_data = get_sales_details(filters)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if sales_invoice_data.get(item.name):
|
||||||
|
item_obj = sales_invoice_data[item.name]
|
||||||
|
if item_obj.days_since_last_order > cint(filters['days']):
|
||||||
|
row = {
|
||||||
|
"territory": item_obj.territory,
|
||||||
|
"item_group": item_obj.item_group,
|
||||||
|
"item": item_obj.name,
|
||||||
|
"item_name": item_obj.item_name,
|
||||||
|
"customer": item_obj.customer,
|
||||||
|
"last_order_date": item_obj.last_order_date,
|
||||||
|
"qty": item_obj.qty,
|
||||||
|
"days_since_last_order": item_obj.days_since_last_order
|
||||||
|
}
|
||||||
|
data.append(row)
|
||||||
|
else:
|
||||||
|
row = {
|
||||||
|
"item_group": item.item_group,
|
||||||
|
"item": item.name,
|
||||||
|
"item_name": item.item_name
|
||||||
|
}
|
||||||
|
data.append(row)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_sales_details(filters):
|
||||||
|
|
||||||
|
data = []
|
||||||
|
item_details_map = {}
|
||||||
|
|
||||||
|
date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date"
|
||||||
|
|
||||||
|
sales_data = frappe.db.sql("""
|
||||||
|
select s.territory, s.customer, si.item_group, si.item_name, si.qty, {date_field} as last_order_date,
|
||||||
|
DATEDIFF(CURDATE(), {date_field}) as days_since_last_order
|
||||||
|
from `tab{doctype}` s, `tab{doctype} Item` si
|
||||||
|
where s.name = si.parent and s.docstatus = 1
|
||||||
|
group by si.name order by days_since_last_order """ #nosec
|
||||||
|
.format(date_field = date_field, doctype = filters['based_on']), as_dict=1)
|
||||||
|
|
||||||
|
for d in sales_data:
|
||||||
|
item_details_map.setdefault(d.item_name, d)
|
||||||
|
|
||||||
|
return item_details_map
|
||||||
|
|
||||||
|
def get_items(filters):
|
||||||
|
|
||||||
|
filters_dict = {
|
||||||
|
"disabled": 0,
|
||||||
|
"is_stock_item": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.get("item_group"):
|
||||||
|
filters_dict.update({
|
||||||
|
"item_group": filters["item_group"]
|
||||||
|
})
|
||||||
|
|
||||||
|
if filters.get("item"):
|
||||||
|
filters_dict.update({
|
||||||
|
"name": filters["item"]
|
||||||
|
})
|
||||||
|
|
||||||
|
items = frappe.get_all("Item", fields=["name", "item_group", "item_name"], filters=filters_dict, order_by="name")
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
Reference in New Issue
Block a user