Merge branch 'v12-pre-release' into version-12

This commit is contained in:
Saurabh
2020-10-22 18:28:33 +05:30
79 changed files with 1588 additions and 1460 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '12.12.1' __version__ = '12.13.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -97,7 +97,7 @@
"default": "1", "default": "1",
"fieldname": "unlink_advance_payment_on_cancelation_of_order", "fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Unlink Advance Payment on Cancelation of Order" "label": "Unlink Advance Payment on Cancellation of Order"
}, },
{ {
"default": "1", "default": "1",
@@ -179,7 +179,7 @@
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"modified": "2020-03-11 13:09:26.235848", "modified": "2020-10-08 09:40:12.121145",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -1,347 +1,122 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 0,
"autoname": "field:year", "autoname": "field:year",
"beta": 0,
"creation": "2013-01-22 16:50:25", "creation": "2013-01-22 16:50:25",
"custom": 0,
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0, "engine": "InnoDB",
"field_order": [
"year",
"disabled",
"is_short_year",
"year_start_date",
"year_end_date",
"companies",
"auto_created"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "For e.g. 2012, 2012-13", "description": "For e.g. 2012, 2012-13",
"fieldname": "year", "fieldname": "year",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Year Name", "label": "Year Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "year", "oldfieldname": "year",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "unique": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Disabled"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_start_date", "fieldname": "year_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Year Start Date", "label": "Year Start Date",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "year_start_date", "oldfieldname": "year_start_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_end_date", "fieldname": "year_end_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Year End Date", "label": "Year End Date",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "companies", "fieldname": "companies",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Companies", "label": "Companies",
"length": 0, "options": "Fiscal Year Company"
"no_copy": 0,
"options": "Fiscal Year Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fieldname": "auto_created", "fieldname": "auto_created",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Auto Created", "label": "Auto Created",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1, },
"remember_last_selected_value": 0, {
"report_hide": 0, "default": "0",
"reqd": 0, "description": "Less than 12 months.",
"search_index": 0, "fieldname": "is_short_year",
"set_only_once": 0, "fieldtype": "Check",
"translatable": 0, "label": "Is Short Year",
"unique": 0 "set_only_once": 1
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"image_view": 0, "modified": "2020-10-03 18:22:04.161315",
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-25 14:21:41.273354",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Fiscal Year", "name": "Fiscal Year",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Sales User"
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Purchase User"
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Accounts User"
"role": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Stock User"
"role": "Stock User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Employee"
"role": "Employee",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "name", "sort_field": "name",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0
} }

View File

@@ -36,6 +36,11 @@ class FiscalYear(Document):
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.")) frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
def validate_dates(self): def validate_dates(self):
if self.is_short_year:
# Fiscal Year can be shorter than one year, in some jurisdictions
# under certain circumstances. For example, in the USA and Germany.
return
if getdate(self.year_start_date) > getdate(self.year_end_date): if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate) FiscalYearIncorrectDate)

View File

@@ -58,5 +58,12 @@
"year": "_Test Fiscal Year 2021", "year": "_Test Fiscal Year 2021",
"year_end_date": "2021-12-31", "year_end_date": "2021-12-31",
"year_start_date": "2021-01-01" "year_start_date": "2021-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2021",
"is_short_year": 1,
"year_end_date": "2021-12-31",
"year_start_date": "2021-04-01"
} }
] ]

View File

@@ -31,13 +31,19 @@ apply_on_table = {
} }
def get_pricing_rules(args, doc=None): def get_pricing_rules(args, doc=None):
pricing_rules = [] pricing_rules_all = []
values = {} values = {}
for apply_on in ['Item Code', 'Item Group', 'Brand']: for apply_on in ['Item Code', 'Item Group', 'Brand']:
pricing_rules.extend(_get_pricing_rules(apply_on, args, values)) pricing_rules_all.extend(_get_pricing_rules(apply_on, args, values))
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
break # removing duplicate pricing rule
pricing_rules_name = []
pricing_rules = []
for p in pricing_rules_all:
if p['name'] not in pricing_rules_name:
pricing_rules_name.append(p['name'])
pricing_rules.append(p)
rules = [] rules = []
@@ -323,9 +329,10 @@ def apply_internal_priority(pricing_rules, field_set, args):
filtered_rules = [] filtered_rules = []
for field in field_set: for field in field_set:
if args.get(field): if args.get(field):
# filter function always returns a filter object even if empty for rule in pricing_rules:
# list conversion is necessary to check for an empty result if rule.get(field) == args.get(field):
filtered_rules = list(filter(lambda x: x.get(field)==args.get(field), pricing_rules)) filtered_rules = [rule]
break
if filtered_rules: break if filtered_rules: break
return filtered_rules or pricing_rules return filtered_rules or pricing_rules

View File

@@ -636,7 +636,8 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate) item.item_tax_amount / self.conversion_rate)
}, item=item)) }, item=item))
else: else:
cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) cwip_account = get_asset_account("capital_work_in_progress_account",
asset_category=item.asset_category,company=self.company)
cwip_account_currency = get_account_currency(cwip_account) cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({

View File

@@ -939,7 +939,8 @@ def make_purchase_invoice(**args):
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "" "rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or ""
}) })
if args.get_taxes_and_charges: if args.get_taxes_and_charges:

View File

@@ -13,8 +13,7 @@ def get_data():
'Auto Repeat': 'reference_document', 'Auto Repeat': 'reference_document',
}, },
'internal_links': { 'internal_links': {
'Sales Order': ['items', 'sales_order'], 'Sales Order': ['items', 'sales_order']
'Delivery Note': ['items', 'delivery_note']
}, },
'transactions': [ 'transactions': [
{ {

View File

@@ -263,13 +263,14 @@ class Subscription(Document):
invoice.set_taxes() invoice.set_taxes()
# Due date # Due date
invoice.append( if self.days_until_due:
'payment_schedule', invoice.append(
{ 'payment_schedule',
'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)), {
'invoice_portion': 100 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
} 'invoice_portion': 100
) }
)
# Discounts # Discounts
if self.additional_discount_percentage: if self.additional_discount_percentage:

View File

@@ -210,6 +210,7 @@ class TestSubscription(unittest.TestCase):
subscription.customer = '_Test Customer' subscription.customer = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.start = '2018-01-01' subscription.start = '2018-01-01'
subscription.days_until_due = 1
subscription.insert() subscription.insert()
subscription.process() # generate first invoice subscription.process() # generate first invoice

View File

@@ -6,6 +6,8 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
test_records = frappe.get_test_records('Tax Rule') test_records = frappe.get_test_records('Tax Rule')
@@ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase):
self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
"_Test Sales Taxes and Charges Template 1 - _TC") "_Test Sales Taxes and Charges Template 1 - _TC")
def test_taxes_fetch_via_tax_rule(self):
make_tax_rule(customer= "_Test Customer", billing_city = "_Test City",
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
# create opportunity for customer
opportunity = make_opportunity(with_items=1)
# make quotation from opportunity
quotation = make_quotation(opportunity.name)
quotation.save()
self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC")
# Check if accounts heads and rate fetched are also fetched from tax template or not
self.assertTrue(len(quotation.taxes) > 0)
def make_tax_rule(**args): def make_tax_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
from `tabGL Entry` from `tabGL Entry`
where company = %s and where company = %s and
party in %s and fiscal_year=%s and credit > 0 party in %s and fiscal_year=%s and credit > 0
and is_opening = 'No'
""", (company, tuple(suppliers), fiscal_year), as_dict=1) """, (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries] vouchers = [d.voucher_no for d in entries]
@@ -139,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
else: else:
tds_amount = _get_tds(net_total, tax_details.rate) tds_amount = _get_tds(net_total, tax_details.rate)
else: else:
supplier_credit_amount = frappe.get_all('Purchase Invoice Item', supplier_credit_amount = frappe.get_all('Purchase Invoice',
fields = ['sum(net_amount)'], fields = ['sum(net_total)'],
filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1) filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
supplier_credit_amount = (supplier_credit_amount[0][0] supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0) if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
@@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No
select distinct voucher_no select distinct voucher_no
from `tabGL Entry` from `tabGL Entry`
where party in %s and %s and debit > 0 where party in %s and %s and debit > 0
and is_opening = 'No'
""", (tuple(suppliers), condition)) or [] """, (tuple(suppliers), condition)) or []
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):

View File

@@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() d.cancel()
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
pi.submit()
invoices.append(pi)
# TDS not applied
pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
pi.submit()
invoices.append(pi)
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
self.assertEqual(pi.grand_total, 8000)
# delete invoices to avoid clashing
for d in invoices:
d.cancel()
def create_purchase_invoice(**args): def create_purchase_invoice(**args):
# return sales invoice doc object # return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
@@ -109,7 +132,7 @@ def create_purchase_invoice(**args):
pi = frappe.get_doc({ pi = frappe.get_doc({
"doctype": "Purchase Invoice", "doctype": "Purchase Invoice",
"posting_date": today(), "posting_date": today(),
"apply_tds": 1, "apply_tds": 0 if args.do_not_apply_tds else 1,
"supplier": args.supplier, "supplier": args.supplier,
"company": '_Test Company', "company": '_Test Company',
"taxes_and_charges": "", "taxes_and_charges": "",

View File

@@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
check_plaid_status() { check_plaid_status() {
const me = this; const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
if (r && r.enabled == "1") { if (r && r.enabled === "1") {
me.plaid_status = "active" me.plaid_status = "active"
} else { } else {
me.plaid_status = "inactive" me.plaid_status = "inactive"
@@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
init_config() { init_config() {
const me = this; const me = this;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
.then(result => { .then(result => {
me.plaid_env = result.plaid_env; me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key; me.client_name = result.client_name;
me.client_name = result.client_name; me.link_token = result.link_token;
me.sync_transactions() me.sync_transactions();
}) })
} }
sync_transactions() { sync_transactions() {
const me = this; const me = this;
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => { frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
bank: v['bank'], bank: r.bank,
bank_account: me.parent.bank_account, bank_account: me.parent.bank_account,
freeze: true freeze: true
}) })
.then((result) => { .then((result) => {
let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") let result_title = (result && result.length > 0)
? __("{0} bank transaction(s) created", [result.length])
: __("This bank account is already synchronized");
let result_msg = ` let result_msg = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;"> <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5> <h5 class="text-muted">${result_title}</h5>
</div>` </div>`
this.parent.$main_section.append(result_msg) this.parent.$main_section.append(result_msg)
frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'}); frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
}) })
}) })
} }
@@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
}) })
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
{bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
).then((result) => { ).then((result) => {
me.make_dialog(result) me.make_dialog(result)
}) })

View File

@@ -1064,7 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
$(frappe.render_template("pos_item", { $(frappe.render_template("pos_item", {
item_code: escape(obj.name), item_code: escape(obj.name),
item_price: item_price, item_price: item_price,
title: obj.name || obj.item_name, title: obj.name === obj.item_name ? obj.name : obj.item_name,
item_name: obj.name === obj.item_name ? "" : obj.item_name, item_name: obj.name === obj.item_name ? "" : obj.item_name,
item_image: obj.image, item_image: obj.image,
item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj), item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
@@ -1546,7 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
$.each(this.frm.doc.items || [], function (i, d) { $.each(this.frm.doc.items || [], function (i, d) {
$(frappe.render_template("pos_bill_item_new", { $(frappe.render_template("pos_bill_item_new", {
item_code: escape(d.item_code), item_code: escape(d.item_code),
title: d.item_code || d.item_name, title: d.item_code === d.item_name ? d.item_code : d.item_name,
item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("<br>" + d.item_name), item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("<br>" + d.item_name),
qty: d.qty, qty: d.qty,
discount_percentage: d.discount_percentage || 0.0, discount_percentage: d.discount_percentage || 0.0,

View File

@@ -69,7 +69,7 @@ frappe.query_reports["Accounts Receivable"] = {
filters: { filters: {
'company': company 'company': company
} }
} };
} }
}, },
{ {

View File

@@ -617,9 +617,19 @@ class ReceivablePayableReport(object):
elif party_type_field=="supplier": elif party_type_field=="supplier":
self.add_supplier_filters(conditions, values) self.add_supplier_filters(conditions, values)
if self.filters.cost_center:
self.get_cost_center_conditions(conditions)
self.add_accounting_dimensions_filters(conditions, values) self.add_accounting_dimensions_filters(conditions, values)
return " and ".join(conditions), values return " and ".join(conditions), values
def get_cost_center_conditions(self, conditions):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})]
cost_center_string = '", "'.join(cost_center_list)
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
def get_order_by_condition(self): def get_order_by_condition(self):
if self.filters.get('group_by_party'): if self.filters.get('group_by_party'):
return "order by party, posting_date" return "order by party, posting_date"

View File

@@ -3,6 +3,14 @@
frappe.query_reports["Bank Reconciliation Statement"] = { frappe.query_reports["Bank Reconciliation Statement"] = {
"filters": [ "filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{ {
"fieldname":"account", "fieldname":"account",
"label": __("Bank Account"), "label": __("Bank Account"),
@@ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
"reqd": 1, "reqd": 1,
"get_query": function() { "get_query": function() {
var company = frappe.query_report.get_filter_value('company')
return { return {
"query": "erpnext.controllers.queries.get_account_list", "query": "erpnext.controllers.queries.get_account_list",
"filters": [ "filters": [
['Account', 'account_type', 'in', 'Bank, Cash'], ['Account', 'account_type', 'in', 'Bank, Cash'],
['Account', 'is_group', '=', 0], ['Account', 'is_group', '=', 0],
['Account', 'disabled', '=', 0],
['Account', 'company', '=', company],
] ]
} }
} }

View File

@@ -44,7 +44,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for(let j=0, k=data.length-1; j<k; j++) { %} {% for(let j=0, k=data.length; j<k; j++) { %}
{% {%
var row = data[j]; var row = data[j];
var row_class = data[j].parent_account ? "" : "financial-statements-important"; var row_class = data[j].parent_account ? "" : "financial-statements-important";

View File

@@ -252,13 +252,6 @@ frappe.ui.form.on('Asset', {
}) })
}, },
available_for_use_date: function(frm) {
$.each(frm.doc.finance_books || [], function(i, d) {
if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date;
});
refresh_field("finance_books");
},
is_existing_asset: function(frm) { is_existing_asset: function(frm) {
frm.trigger("toggle_reference_doc"); frm.trigger("toggle_reference_doc");
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
@@ -437,6 +430,15 @@ frappe.ui.form.on('Asset Finance Book', {
} }
frappe.flags.dont_change_rate = false; frappe.flags.dont_change_rate = false;
},
depreciation_start_date: function(frm, cdt, cdn) {
const book = locals[cdt][cdn];
if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) {
frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`));
book.depreciation_start_date = "";
frm.refresh_field("finance_books");
}
} }
}); });

View File

@@ -84,6 +84,11 @@ class Asset(AccountsController):
if not self.available_for_use_date: if not self.available_for_use_date:
frappe.throw(_("Available for use date is required")) frappe.throw(_("Available for use date is required"))
for d in self.finance_books:
if d.depreciation_start_date == self.available_for_use_date:
frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx),
title=_("Incorrect Date"))
def set_missing_values(self): def set_missing_values(self):
if not self.asset_category: if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
@@ -312,7 +317,7 @@ class Asset(AccountsController):
if not row.depreciation_start_date: if not row.depreciation_start_date:
if not self.available_for_use_date: if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
row.depreciation_start_date = self.available_for_use_date row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset: if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0 self.opening_accumulated_depreciation = 0
@@ -465,29 +470,37 @@ class Asset(AccountsController):
def validate_make_gl_entry(self): def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
asset_bought_with_invoice = purchase_document == self.purchase_invoice if not purchase_document:
fixed_asset_account, cwip_account = self.get_asset_accounts()
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
# check if expense already has been booked in case of cwip was enabled after purchasing asset
expense_booked = False
cwip_booked = False
if asset_bought_with_invoice:
expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
(purchase_document, fixed_asset_account), as_dict=1)
else:
cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
(purchase_document, cwip_account), as_dict=1)
if cwip_enabled and (expense_booked or not cwip_booked):
# if expense has already booked from invoice or cwip is booked from receipt
return False return False
elif not cwip_enabled and (not expense_booked or cwip_booked):
# if cwip is disabled but expense hasn't been booked yet asset_bought_with_invoice = (purchase_document == self.purchase_invoice)
return True fixed_asset_account = self.get_fixed_asset_account()
elif cwip_enabled:
# default condition cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
return True cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled)
query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s"""
if asset_bought_with_invoice:
# with invoice purchase either expense or cwip has been booked
expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1)
if expense_booked:
# if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled
return False
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
if cwip_booked:
# if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled
return True
else:
# with receipt purchase either cwip has been booked or no entries have been made
if not cwip_account:
# if cwip account isn't available do not make gl entries
return False
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
# if cwip is not booked from receipt then do not make gl entries
# if cwip is booked from receipt then make gl entries
return cwip_booked
def get_purchase_document(self): def get_purchase_document(self):
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
@@ -495,20 +508,25 @@ class Asset(AccountsController):
return purchase_document return purchase_document
def get_asset_accounts(self): def get_fixed_asset_account(self):
fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
asset_category = self.asset_category, company = self.company)
cwip_account = get_asset_account("capital_work_in_progress_account", def get_cwip_account(self, cwip_enabled=False):
self.name, self.asset_category, self.company) cwip_account = None
try:
cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company)
except:
# if no cwip account found in category or company and "cwip is enabled" then raise else silently pass
if cwip_enabled:
raise
return fixed_asset_account, cwip_account return cwip_account
def make_gl_entries(self): def make_gl_entries(self):
gl_entries = [] gl_entries = []
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_asset_accounts() fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):

View File

@@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
from erpnext.assets.doctype.asset.asset import make_sales_invoice from erpnext.assets.doctype.asset.asset import make_sales_invoice
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
class TestAsset(unittest.TestCase): class TestAsset(unittest.TestCase):
@@ -374,19 +375,18 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = nowdate() asset.available_for_use_date = '2020-01-01'
asset.purchase_date = nowdate() asset.purchase_date = '2020-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 10,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 1
"depreciation_start_date": nowdate()
}) })
asset.insert() asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date=add_months(nowdate(), 10)) post_depreciation_entries(date=add_months('2020-01-01', 4))
scrap_asset(asset.name) scrap_asset(asset.name)
@@ -395,9 +395,9 @@ class TestAsset(unittest.TestCase):
self.assertTrue(asset.journal_entry_for_scrap) self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@@ -469,8 +469,7 @@ class TestAsset(unittest.TestCase):
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10
"depreciation_start_date": "2020-06-06"
}) })
asset.insert() asset.insert()
accumulated_depreciation_after_full_schedule = \ accumulated_depreciation_after_full_schedule = \
@@ -563,81 +562,6 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle) self.assertEqual(gle, expected_gle)
def test_gle_with_cwip_toggling(self):
# TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=5000, do_not_submit=True, location="Test Location")
pr.set('taxes', [{
'category': 'Total',
'add_deduct_tax': 'Add',
'charge_type': 'On Net Total',
'account_head': '_Test Account Service Tax - _TC',
'description': '_Test Account Service Tax',
'cost_center': 'Main - _TC',
'rate': 5.0
}, {
'category': 'Valuation and Total',
'add_deduct_tax': 'Add',
'charge_type': 'On Net Total',
'account_head': '_Test Account Shipping Charges - _TC',
'description': '_Test Account Shipping Charges',
'cost_center': 'Main - _TC',
'rate': 5.0
}])
pr.submit()
expected_gle = (
("Asset Received But Not Billed - _TC", 0.0, 5250.0),
("CWIP Account - _TC", 5250.0, 0.0)
)
pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no = %s
order by account""", pr.name)
self.assertEqual(pr_gle, expected_gle)
pi = make_invoice(pr.name)
pi.submit()
expected_gle = (
("_Test Account Service Tax - _TC", 250.0, 0.0),
("_Test Account Shipping Charges - _TC", 250.0, 0.0),
("Asset Received But Not Billed - _TC", 5250.0, 0.0),
("Creditors - _TC", 0.0, 5500.0),
("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
)
pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no = %s
order by account""", pi.name)
self.assertEqual(pi_gle, expected_gle)
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
month_end_date = get_last_day(nowdate())
asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
asset_doc.append("finance_books", {
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10,
"depreciation_start_date": month_end_date
})
# disable cwip and try submitting
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
asset_doc.submit()
# asset should have gl entries even if cwip is disabled
expected_gle = (
("_Test Fixed Asset - _TC", 5250.0, 0.0),
("CWIP Account - _TC", 0.0, 5250.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Asset' and voucher_no = %s
order by account""", asset_doc.name)
self.assertEqual(gle, expected_gle)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
def test_expense_head(self): def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location") qty=2, rate=200000.0, location="Test Location")
@@ -646,6 +570,74 @@ class TestAsset(unittest.TestCase):
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
def test_asset_cwip_toggling_cases(self):
cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"])
cwip_acc = "CWIP Account - _TC"
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "")
# case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertFalse(gle)
# case 1 -- PR with cwip disabled, Asset with cwip enabled
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertFalse(gle)
# case 2 -- PR with cwip enabled, Asset with cwip disabled
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertTrue(gle)
# case 3 -- PI with cwip disabled, Asset with cwip enabled
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertFalse(gle)
# case 4 -- PI with cwip enabled, Asset with cwip disabled
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertTrue(gle)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
def create_asset_data(): def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"): if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category() create_asset_category()

View File

@@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
class AssetCategory(Document): class AssetCategory(Document):
@@ -13,6 +13,7 @@ class AssetCategory(Document):
self.validate_finance_books() self.validate_finance_books()
self.validate_account_types() self.validate_account_types()
self.validate_account_currency() self.validate_account_currency()
self.valide_cwip_account()
def validate_finance_books(self): def validate_finance_books(self):
for d in self.finance_books: for d in self.finance_books:
@@ -59,6 +60,21 @@ class AssetCategory(Document):
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
title=_("Invalid Account")) title=_("Invalid Account"))
def valide_cwip_account(self):
if self.enable_cwip_accounting:
missing_cwip_accounts_for_company = []
for d in self.accounts:
if (not d.capital_work_in_progress_account and
not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")):
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
if missing_cwip_accounts_for_company:
msg = _("""To enable Capital Work in Progress Accounting, """)
msg += _("""you must select Capital Work in Progress Account in accounts table""")
msg += "<br><br>"
msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company))
frappe.throw(msg, title=_("Missing Account"))
@frappe.whitelist() @frappe.whitelist()
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None): def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):

View File

@@ -27,3 +27,21 @@ class TestAssetCategory(unittest.TestCase):
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
def test_cwip_accounting(self):
company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account")
frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "")
asset_category = frappe.new_doc("Asset Category")
asset_category.asset_category_name = "Computers"
asset_category.enable_cwip_accounting = 1
asset_category.total_number_of_depreciations = 3
asset_category.frequency_of_depreciation = 3
asset_category.append("accounts", {
"company_name": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
"depreciation_expense_account": "_Test Depreciations - _TC"
})
self.assertRaises(frappe.ValidationError, asset_category.insert)

View File

@@ -1,347 +1,99 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-05-08 14:44:37.095570", "creation": "2018-05-08 14:44:37.095570",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
"expected_value_after_useful_life",
"value_after_depreciation",
"rate_of_depreciation"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Finance Book", "label": "Finance Book",
"length": 0, "options": "Finance Book"
"no_copy": 0,
"options": "Finance Book",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "depreciation_method", "fieldname": "depreciation_method",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Depreciation Method", "label": "Depreciation Method",
"length": 0,
"no_copy": 0,
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "total_number_of_depreciations", "fieldname": "total_number_of_depreciations",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Number of Depreciations", "label": "Total Number of Depreciations",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "frequency_of_depreciation", "fieldname": "frequency_of_depreciation",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Frequency of Depreciation (Months)", "label": "Frequency of Depreciation (Months)",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:parent.doctype == 'Asset'", "depends_on": "eval:parent.doctype == 'Asset'",
"fetch_if_empty": 0,
"fieldname": "depreciation_start_date", "fieldname": "depreciation_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Depreciation Posting Date",
"label": "Depreciation Start Date", "reqd": 1
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"depends_on": "eval:parent.doctype == 'Asset'", "depends_on": "eval:parent.doctype == 'Asset'",
"fetch_if_empty": 0,
"fieldname": "expected_value_after_useful_life", "fieldname": "expected_value_after_useful_life",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expected Value After Useful Life", "label": "Expected Value After Useful Life",
"length": 0, "options": "Company:company:default_currency"
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "value_after_depreciation", "fieldname": "value_after_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Value After Depreciation", "label": "Value After Depreciation",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'", "depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"description": "In Percentage", "description": "In Percentage",
"fetch_if_empty": 0,
"fieldname": "rate_of_depreciation", "fieldname": "rate_of_depreciation",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "label": "Rate of Depreciation"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rate of Depreciation",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-04-09 19:45:14.523488", "modified": "2020-09-16 12:11:30.631788",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase):
"next_depreciation_date": "2020-12-31", "next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10
"depreciation_start_date": "2020-06-06"
}) })
if asset.docstatus == 0: if asset.docstatus == 0:
@@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase):
"next_depreciation_date": "2020-12-31", "next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10
"depreciation_start_date": "2020-06-06"
}) })
if asset.docstatus == 0: if asset.docstatus == 0:
asset.submit() asset.submit()

View File

@@ -202,6 +202,91 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
frappe.set_user("Administrator") frappe.set_user("Administrator")
def test_update_child_with_tax_template(self):
"""
Test Action: Create a PO with one item having its tax account head already in the PO.
Add the same item + new item with tax template via Update Items.
Expected result: First Item's tax row is updated. New tax row is added for second Item.
"""
if not frappe.db.exists("Item", "Test Item with Tax"):
make_item("Test Item with Tax", {
'is_stock_item': 1,
})
if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
frappe.get_doc({
'doctype': 'Item Tax Template',
'title': 'Test Update Items Template',
'company': '_Test Company',
'taxes': [
{
'tax_type': "_Test Account Service Tax - _TC",
'tax_rate': 10,
}
]
}).insert()
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", {
"item_tax_template": "Test Update Items Template",
"valid_from": nowdate()
})
new_item_with_tax.save()
tax_template = "_Test Account Excise Duty @ 10"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
item_doc.append("taxes", {
"item_tax_template": tax_template,
"valid_from": nowdate()
})
item_doc.save()
else:
# update valid from
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template})
po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
po.append("taxes", {
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Purchase Taxes and Charges",
"rate": 10
})
po.insert()
po.submit()
self.assertEqual(po.taxes[0].tax_amount, 50)
self.assertEqual(po.taxes[0].total, 550)
items = json.dumps([
{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
{'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO
{'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
])
update_child_qty_rate('Purchase Order', items, po.name)
po.reload()
self.assertEqual(po.taxes[0].tax_amount, 70)
self.assertEqual(po.taxes[0].total, 770)
self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC")
self.assertEqual(po.taxes[1].tax_amount, 70)
self.assertEqual(po.taxes[1].total, 840)
# teardown
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
po.cancel()
po.delete()
new_item_with_tax.delete()
frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
def test_update_qty(self): def test_update_qty(self):
po = create_purchase_order() po = create_purchase_order()
@@ -733,6 +818,59 @@ class TestPurchaseOrder(unittest.TestCase):
update_backflush_based_on("BOM") update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self):
item_code = "_Test Subcontracted FG Item 5"
make_item('Sub Contracted Raw Material 4', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 250
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True)
# Add same subcontracted items multiple times
po.append("items", {
"item_code": item_code,
"qty": order_qty,
"schedule_date": add_days(nowdate(), 1),
"warehouse": "_Test Warehouse - _TC"
})
po.set_missing_values()
po.submit()
# Material receipt entry for the raw materials which will be send to supplier
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100)
rm_items = [
{
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name
},
{
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name
},
]
# Raw Materials transfer entry from stores to supplier's warehouse
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
po_doc = frappe.get_doc("Purchase Order", po.name)
for row in po_doc.supplied_items:
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
self.assertEqual(row.supplied_qty, 250.0)
update_backflush_based_on("BOM")
def test_advance_payment_entry_unlink_against_purchase_order(self): def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
frappe.db.set_value("Accounts Settings", "Accounts Settings", frappe.db.set_value("Accounts Settings", "Accounts Settings",

View File

@@ -143,7 +143,7 @@ def get_conditions(filters):
conditions = "" conditions = ""
if filters.get("company"): if filters.get("company"):
conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company')) conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company'))
if filters.get("cost_center") or filters.get("project"): if filters.get("cost_center") or filters.get("project"):
conditions += """ conditions += """
@@ -151,10 +151,10 @@ def get_conditions(filters):
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
if filters.get("from_date"): if filters.get("from_date"):
conditions += " AND par.transaction_date>='%s'" % filters.get('from_date') conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date')
if filters.get("to_date"): if filters.get("to_date"):
conditions += " AND par.transaction_date<='%s'" % filters.get('to_date') conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date')
return conditions return conditions
def get_data(filters): def get_data(filters):
@@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions):
mr_records = {} mr_records = {}
mr_details = frappe.db.sql(""" mr_details = frappe.db.sql("""
SELECT SELECT
par.transaction_date, parent.transaction_date,
par.per_ordered, parent.per_ordered,
par.owner, parent.owner,
child.name, child.name,
child.parent, child.parent,
child.amount, child.amount,
child.qty, child.qty,
child.item_code, child.item_code,
child.uom, child.uom,
par.status parent.status,
FROM `tabMaterial Request` par, `tabMaterial Request Item` child child.project,
child.cost_center
FROM `tabMaterial Request` parent, `tabMaterial Request Item` child
WHERE WHERE
par.per_ordered>=0 parent.per_ordered>=0
AND par.name=child.parent AND parent.name=child.parent
AND par.docstatus=1 AND parent.docstatus=1
{conditions} {conditions}
""".format(conditions=conditions), as_dict=1) #nosec """.format(conditions=conditions), as_dict=1) #nosec
@@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions):
status=record.status, status=record.status,
actual_cost=0, actual_cost=0,
purchase_order_amt=0, purchase_order_amt=0,
purchase_order_amt_in_company_currency=0 purchase_order_amt_in_company_currency=0,
project = record.project,
cost_center = record.cost_center
) )
procurement_record_against_mr.append(procurement_record_details) procurement_record_against_mr.append(procurement_record_details)
return mr_records, procurement_record_against_mr return mr_records, procurement_record_against_mr
@@ -280,16 +284,16 @@ def get_po_entries(conditions):
child.amount, child.amount,
child.base_amount, child.base_amount,
child.schedule_date, child.schedule_date,
par.transaction_date, parent.transaction_date,
par.supplier, parent.supplier,
par.status, parent.status,
par.owner parent.owner
FROM `tabPurchase Order` par, `tabPurchase Order Item` child FROM `tabPurchase Order` parent, `tabPurchase Order Item` child
WHERE WHERE
par.docstatus = 1 parent.docstatus = 1
AND par.name = child.parent AND parent.name = child.parent
AND par.status not in ("Closed","Completed","Cancelled") AND parent.status not in ("Closed","Completed","Cancelled")
{conditions} {conditions}
GROUP BY GROUP BY
par.name, child.item_code parent.name, child.item_code
""".format(conditions=conditions), as_dict=1) #nosec """.format(conditions=conditions), as_dict=1) #nosec

View File

@@ -0,0 +1,51 @@
## ERPNext v12.13.0 Release Note
### Fixes and Enhancements
- Warehouse address filtered based on warehouse ([#23381](https://github.com/frappe/erpnext/pull/23381))
- COGS validation in the purchase receipt ([#23536](https://github.com/frappe/erpnext/pull/23536))
- Item Tax Updating via `Update Items` in SO/PO ([#23338](https://github.com/frappe/erpnext/pull/23338))
- Pricing rule selector is wrong ([#22328](https://github.com/frappe/erpnext/pull/22328))
- Adding filters validation Batch-Wise Balance History ([#23396](https://github.com/frappe/erpnext/pull/23396))
- Add company and correct filter in bank statement reconciliation report filters ([#23618](https://github.com/frappe/erpnext/pull/23618))
- Incorrect consumed qty if raw material with batch ([#23389](https://github.com/frappe/erpnext/pull/23389))
- Balance serial nos in stock ledger report ([#23520](https://github.com/frappe/erpnext/pull/23520))
- Use Plaid's new API (v12) ([#23317](https://github.com/frappe/erpnext/pull/23317))
- Cost Center filter in accounts receivable and payable report ([#23356](https://github.com/frappe/erpnext/pull/23356))
- Incorrect operation time calculation for batch size ([#23480](https://github.com/frappe/erpnext/pull/23480))
- Serial no field is blank in stock reconciliation ([#23648](https://github.com/frappe/erpnext/pull/23648))
- Manually set serial nos override with current available serial nos ([#23657](https://github.com/frappe/erpnext/pull/23657))
- Can't save item price after adding child table ([#23594](https://github.com/frappe/erpnext/pull/23594))
- Naming series - cannot reset current value to zero ([#23505](https://github.com/frappe/erpnext/pull/23505))
- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23463](https://github.com/frappe/erpnext/pull/23463))
- Taxes not getting fetched from Opportunity to Quotation ([#23354](https://github.com/frappe/erpnext/pull/23354))
- Stock reconciliation, incorrect serial nos fetched in the current serial no field ([#23366](https://github.com/frappe/erpnext/pull/23366))
- Cannot create asset if cwip disabled and account not set ([#23584](https://github.com/frappe/erpnext/pull/23584))
- Leave application status fix ([#23411](https://github.com/frappe/erpnext/pull/23411))
- Not able to do overproduction ([#23600](https://github.com/frappe/erpnext/pull/23600))
- Incorrect supplied qty error ([#23619](https://github.com/frappe/erpnext/pull/23619))
- Download Required Materials not working for production plan ([#23403](https://github.com/frappe/erpnext/pull/23403))
- Make account number length configurable ([#23496](https://github.com/frappe/erpnext/pull/23496))
- Include item_code in items result to allow adding product info in custom templates ([#23440](https://github.com/frappe/erpnext/pull/23440))
- Mode of payment getting overwritten by default mode of payment for returns ([#23599](https://github.com/frappe/erpnext/pull/23599))
- Show total row in print format of financial statement ([#23565](https://github.com/frappe/erpnext/pull/23565))
- Performance issue while adding template item in the cart ([#23508](https://github.com/frappe/erpnext/pull/23508))
- Display item name instead of item code in offline POS ([#23451](https://github.com/frappe/erpnext/pull/23451))
- Set stock UOM in item while creating material request from stock entry ([#23430](https://github.com/frappe/erpnext/pull/23430))
- Book loss amount in the COGS instead of stock received but not billed ([#23412](https://github.com/frappe/erpnext/pull/23412))
- Depreciation start date ux fixes ([#23340](https://github.com/frappe/erpnext/pull/23340))
- Payment Schedule not fetching ([#23477](https://github.com/frappe/erpnext/pull/23477))
- Taxable value in GSTR 3B report ([#23552](https://github.com/frappe/erpnext/pull/23552))
- Negative stock error while submitting stock reco for batch item ([#23564](https://github.com/frappe/erpnext/pull/23564))
- Check only Read and Write Permission in Update Items ([#23458](https://github.com/frappe/erpnext/pull/23458))
- Handle Blank from/to range in Numeric Item Attribute ([#23484](https://github.com/frappe/erpnext/pull/23484))
- Added new filters in the Batch-wise balance history report ([#23522](https://github.com/frappe/erpnext/pull/23522))
- Added filter show in website for filtering product ([#23638](https://github.com/frappe/erpnext/pull/23638))
- Enabled no copy property for Supplier Invoice Date to avoid due date validation ([#23367](https://github.com/frappe/erpnext/pull/23367))
- Showing a negative balance in expired leaves ([#23426](https://github.com/frappe/erpnext/pull/23426))
- Validate if removed item attributes exist in variants ([#23591](https://github.com/frappe/erpnext/pull/23591))
- Longer timeout for company replace abbreviation ([#23442](https://github.com/frappe/erpnext/pull/23442))
- Online pos print not working ([#23377](https://github.com/frappe/erpnext/pull/23377))
- Last purchase rate in item prices report ([#23507](https://github.com/frappe/erpnext/pull/23507))
- Do not consider opening entries for TDS calculation ([#23598](https://github.com/frappe/erpnext/pull/23598))
- Project value is missing from procurement-tracker ([#23551](https://github.com/frappe/erpnext/pull/23551))

View File

@@ -20,7 +20,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from six import text_type from six import text_type
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
from erpnext.stock.get_item_details import get_item_warehouse from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
@@ -1127,6 +1127,43 @@ def get_supplier_block_status(party_name):
} }
return info return info
def set_child_tax_template_and_map(item, child_item, parent_doc):
args = {
'item_code': item.item_code,
'posting_date': parent_doc.transaction_date,
'tax_category': parent_doc.get('tax_category'),
'company': parent_doc.get('company')
}
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
def add_taxes_from_tax_template(child_item, parent_doc):
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
tax_map = json.loads(child_item.get("item_tax_rate"))
for tax_type in tax_map:
tax_rate = flt(tax_map[tax_type])
taxes = parent_doc.get('taxes') or []
# add new row for tax head only if missing
found = any(tax.account_head == tax_type for tax in taxes)
if not found:
tax_row = parent_doc.append("taxes", {})
tax_row.update({
"description" : str(tax_type).split(' - ')[0],
"charge_type" : "On Net Total",
"account_head" : tax_type,
"rate" : tax_rate
})
if parent_doc.doctype == "Purchase Order":
tax_row.update({
"category" : "Total",
"add_deduct_tax" : "Add"
})
tax_row.db_insert()
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
""" """
Returns a Sales Order Item child item containing the default values Returns a Sales Order Item child item containing the default values
@@ -1140,6 +1177,8 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.uom = item.stock_uom child_item.uom = item.stock_uom
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse: if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
@@ -1162,6 +1201,8 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.uom = item.stock_uom child_item.uom = item.stock_uom
child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
return child_item return child_item
def validate_and_delete_children(parent, data): def validate_and_delete_children(parent, data):
@@ -1270,19 +1311,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
validate_quantity(child_item, d) validate_quantity(child_item, d)
child_item.qty = flt(d.get("qty")) child_item.qty = flt(d.get("qty"))
precision = child_item.precision("rate") or 2 rate_precision = child_item.precision("rate") or 2
conv_fac_precision = child_item.precision("conversion_factor") or 2
qty_precision = child_item.precision("qty") or 2
if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision): if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision):
frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
.format(child_item.idx, child_item.item_code)) .format(child_item.idx, child_item.item_code))
else: else:
child_item.rate = flt(d.get("rate")) child_item.rate = flt(d.get("rate"), rate_precision)
if d.get("conversion_factor"): if d.get("conversion_factor"):
if child_item.stock_uom == child_item.uom: if child_item.stock_uom == child_item.uom:
child_item.conversion_factor = 1 child_item.conversion_factor = 1
else: else:
child_item.conversion_factor = flt(d.get('conversion_factor')) child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision)
if d.get("delivery_date") and parent_doctype == 'Sales Order': if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date') child_item.delivery_date = d.get('delivery_date')

View File

@@ -10,6 +10,7 @@ from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.doctype.item.item import set_item_default
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
@@ -53,10 +54,10 @@ class SellingController(StockController):
super(SellingController, self).set_missing_values(for_validate) super(SellingController, self).set_missing_values(for_validate)
# set contact and address details for customer, if they are not mentioned # set contact and address details for customer, if they are not mentioned
self.set_missing_lead_customer_details() self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate) self.set_price_list_and_item_details(for_validate=for_validate)
def set_missing_lead_customer_details(self): def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None customer, lead = None, None
if getattr(self, "customer", None): if getattr(self, "customer", None):
customer = self.customer customer = self.customer
@@ -93,6 +94,11 @@ class SellingController(StockController):
posting_date=self.get('transaction_date') or self.get('posting_date'), posting_date=self.get('transaction_date') or self.get('posting_date'),
company=self.company)) company=self.company))
if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate:
taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges)
for tax in taxes:
self.append('taxes', tax)
def set_price_list_and_item_details(self, for_validate=False): def set_price_list_and_item_details(self, for_validate=False):
self.set_price_list_currency("Selling") self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate) self.set_missing_item_details(for_validate=for_validate)

View File

@@ -629,22 +629,29 @@ class calculate_taxes_and_totals(object):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay): def update_paid_amount_for_return(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', existing_amount = 0
{'parent': self.doc.pos_profile, 'default': 1},
['mode_of_payment', 'type', 'account'], as_dict=1)
self.doc.payments = [] for payment in self.doc.payments:
existing_amount += payment.amount
if default_mode_of_payment: # do not override user entered amount if equal to total_amount_to_pay
self.doc.append('payments', { if existing_amount != total_amount_to_pay:
'mode_of_payment': default_mode_of_payment.mode_of_payment, default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
'type': default_mode_of_payment.type, {'parent': self.doc.pos_profile, 'default': 1},
'account': default_mode_of_payment.account, ['mode_of_payment', 'type', 'account'], as_dict=1)
'amount': total_amount_to_pay
}) self.doc.payments = []
else:
self.doc.is_pos = 0 if default_mode_of_payment:
self.doc.pos_profile = '' self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'type': default_mode_of_payment.type,
'account': default_mode_of_payment.account,
'amount': total_amount_to_pay
})
else:
self.doc.is_pos = 0
self.doc.pos_profile = ''
self.calculate_paid_amount() self.calculate_paid_amount()

View File

@@ -2,81 +2,90 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals import plaid
from frappe import _ import requests
from frappe.utils.password import get_decrypted_password from plaid.errors import APIError, ItemError, InvalidRequestError
from plaid import Client
from plaid.errors import APIError, ItemError
import frappe import frappe
import requests from frappe import _
class PlaidConnector(): class PlaidConnector():
def __init__(self, access_token=None): def __init__(self, access_token=None):
plaid_settings = frappe.get_single("Plaid Settings")
self.config = {
"plaid_client_id": plaid_settings.plaid_client_id,
"plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'),
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env
}
self.client = Client(client_id=self.config.get("plaid_client_id"),
secret=self.config.get("plaid_secret"),
public_key=self.config.get("plaid_public_key"),
environment=self.config.get("plaid_env")
)
self.access_token = access_token self.access_token = access_token
self.settings = frappe.get_single("Plaid Settings")
self.products = ["auth", "transactions"]
self.client_name = frappe.local.site
self.client = plaid.Client(
client_id=self.settings.plaid_client_id,
secret=self.settings.get_password("plaid_secret"),
environment=self.settings.plaid_env,
api_version="2019-05-29"
)
def get_access_token(self, public_token): def get_access_token(self, public_token):
if public_token is None: if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error")) frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
response = self.client.Item.public_token.exchange(public_token) response = self.client.Item.public_token.exchange(public_token)
access_token = response['access_token'] access_token = response["access_token"]
return access_token return access_token
def get_link_token(self):
token_request = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
"user": {
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
}
}
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:
frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error"))
frappe.msgprint(_("Please check your Plaid client ID and secret values"))
except APIError as e:
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.throw(_(str(e)), title=_("Authentication Failed"))
else:
return response["link_token"]
def auth(self): def auth(self):
try: try:
self.client.Auth.get(self.access_token) self.client.Auth.get(self.access_token)
print("Authentication successful.....")
except ItemError as e: except ItemError as e:
if e.code == 'ITEM_LOGIN_REQUIRED': if e.code == "ITEM_LOGIN_REQUIRED":
pass
else:
pass pass
except APIError as e: except APIError as e:
if e.code == 'PLANNED_MAINTENANCE': if e.code == "PLANNED_MAINTENANCE":
pass
else:
pass pass
except requests.Timeout: except requests.Timeout:
pass pass
except Exception as e: except Exception as e:
print(e)
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'}) frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None): def get_transactions(self, start_date, end_date, account_id=None):
self.auth()
kwargs = dict(
access_token=self.access_token,
start_date=start_date,
end_date=end_date
)
if account_id:
kwargs.update(dict(account_ids=[account_id]))
try: try:
self.auth() response = self.client.Transactions.get(**kwargs)
if account_id: transactions = response["transactions"]
account_ids = [account_id] while len(transactions) < response["total_transactions"]:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
else:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date)
transactions = response['transactions']
while len(transactions) < response['total_transactions']:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response['transactions']) transactions.extend(response["transactions"])
return transactions return transactions
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@@ -4,14 +4,14 @@
frappe.provide("erpnext.integrations"); frappe.provide("erpnext.integrations");
frappe.ui.form.on('Plaid Settings', { frappe.ui.form.on('Plaid Settings', {
enabled: function(frm) { enabled: function (frm) {
frm.toggle_reqd('plaid_client_id', frm.doc.enabled); frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
frm.toggle_reqd('plaid_secret', frm.doc.enabled); frm.toggle_reqd('plaid_secret', frm.doc.enabled);
frm.toggle_reqd('plaid_public_key', frm.doc.enabled);
frm.toggle_reqd('plaid_env', frm.doc.enabled); frm.toggle_reqd('plaid_env', frm.doc.enabled);
}, },
refresh: function(frm) {
if(frm.doc.enabled) { refresh: function (frm) {
if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => { frm.add_custom_button('Link a new bank account', () => {
new erpnext.integrations.plaidLink(frm); new erpnext.integrations.plaidLink(frm);
}); });
@@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', {
erpnext.integrations.plaidLink = class plaidLink { erpnext.integrations.plaidLink = class plaidLink {
constructor(parent) { constructor(parent) {
this.frm = parent; this.frm = parent;
this.product = ["transactions", "auth"];
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config(); this.init_config();
} }
init_config() { async init_config() {
const me = this; this.product = ["auth", "transactions"];
me.plaid_env = me.frm.doc.plaid_env; this.plaid_env = this.frm.doc.plaid_env;
me.plaid_public_key = me.frm.doc.plaid_public_key; this.client_name = frappe.boot.sitename;
me.client_name = frappe.boot.sitename; this.token = await this.frm.call("get_link_token").then(resp => resp.message);
me.init_plaid(); this.init_plaid();
} }
init_plaid() { init_plaid() {
@@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
onScriptLoaded(me) { onScriptLoaded(me) {
me.linkHandler = window.Plaid.create({ me.linkHandler = Plaid.create({
clientName: me.client_name, clientName: me.client_name,
product: me.product,
env: me.plaid_env, env: me.plaid_env,
key: me.plaid_public_key, token: me.token,
onSuccess: me.plaid_success, onSuccess: me.plaid_success
product: me.product
}); });
} }
onScriptError(error) { onScriptError(error) {
frappe.msgprint('There was an issue loading the link-initialize.js script'); frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(error); frappe.msgprint(error);
} }
@@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink {
const me = this; const me = this;
frappe.prompt({ frappe.prompt({
fieldtype:"Link", fieldtype: "Link",
options: "Company", options: "Company",
label:__("Company"), label: __("Company"),
fieldname:"company", fieldname: "company",
reqd:1 reqd: 1
}, (data) => { }, (data) => {
me.company = data.company; me.company = data.company;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response}) frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {
.then((result) => { token: token,
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response, response: response
bank: result, company: me.company}); }).then((result) => {
}) frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {
.then(() => { response: response,
frappe.show_alert({message:__("Bank accounts added"), indicator:'green'}); bank: result,
company: me.company
}); });
}).then(() => {
frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' });
});
}, __("Select a company"), __("Continue")); }, __("Select a company"), __("Continue"));
} }
}; };

View File

@@ -1,5 +1,4 @@
{ {
"actions": [],
"creation": "2018-10-25 10:02:48.656165", "creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -12,7 +11,6 @@
"plaid_client_id", "plaid_client_id",
"plaid_secret", "plaid_secret",
"column_break_7", "column_break_7",
"plaid_public_key",
"plaid_env" "plaid_env"
], ],
"fields": [ "fields": [
@@ -41,12 +39,6 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Plaid Secret" "label": "Plaid Secret"
}, },
{
"fieldname": "plaid_public_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Plaid Public Key"
},
{ {
"fieldname": "plaid_env", "fieldname": "plaid_env",
"fieldtype": "Select", "fieldtype": "Select",
@@ -69,8 +61,7 @@
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "modified": "2020-09-12 02:31:44.542385",
"modified": "2020-02-07 15:21:11.616231",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Plaid Settings", "name": "Plaid Settings",

View File

@@ -2,30 +2,36 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json import json
from frappe import _
from frappe.model.document import Document import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
from frappe.utils import getdate, formatdate, today, add_months from frappe import _
from frappe.desk.doctype.tag.tag import add_tag from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
class PlaidSettings(Document): class PlaidSettings(Document):
pass @staticmethod
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
@frappe.whitelist() @frappe.whitelist()
def plaid_configuration(): def get_plaid_configuration():
if frappe.db.get_single_value("Plaid Settings", "enabled"): if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings") plaid_settings = frappe.get_single("Plaid Settings")
return { return {
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env, "plaid_env": plaid_settings.plaid_env,
"link_token": plaid_settings.get_link_token(),
"client_name": frappe.local.site "client_name": frappe.local.site
} }
else:
return "disabled" return "disabled"
@frappe.whitelist() @frappe.whitelist()
def add_institution(token, response): def add_institution(token, response):
@@ -33,6 +39,7 @@ def add_institution(token, response):
plaid = PlaidConnector() plaid = PlaidConnector()
access_token = plaid.get_access_token(token) access_token = plaid.get_access_token(token)
bank = None
if not frappe.db.exists("Bank", response["institution"]["name"]): if not frappe.db.exists("Bank", response["institution"]["name"]):
try: try:
@@ -44,7 +51,6 @@ def add_institution(token, response):
bank.insert() bank.insert()
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
else: else:
bank = frappe.get_doc("Bank", response["institution"]["name"]) bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token bank.plaid_access_token = access_token
@@ -52,6 +58,7 @@ def add_institution(token, response):
return bank return bank
@frappe.whitelist() @frappe.whitelist()
def add_bank_accounts(response, bank, company): def add_bank_accounts(response, bank, company):
try: try:
@@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
new_account.insert() new_account.insert()
result.append(new_account.name) result.append(new_account.name)
except frappe.UniqueValidationError: except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name)) frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
@@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
return result return result
def add_account_type(account_type): def add_account_type(account_type):
try: try:
frappe.get_doc({ frappe.get_doc({
@@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
@frappe.whitelist() @frappe.whitelist()
def sync_transactions(bank, bank_account): def sync_transactions(bank, bank_account):
''' Sync transactions based on the last integration date as the start date, after sync is completed """Sync transactions based on the last integration date as the start date, after sync is completed
add the transaction date of the oldest transaction as the last integration date ''' add the transaction date of the oldest transaction as the last integration date."""
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_transaction_date: if last_transaction_date:
@@ -148,10 +156,10 @@ def sync_transactions(bank, bank_account):
len(result), bank_account, start_date, end_date)) len(result), bank_account, start_date, end_date))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
def get_transactions(bank, bank_account=None, start_date=None, end_date=None): def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None access_token = None
@@ -169,6 +177,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions return transactions
def new_bank_transaction(transaction): def new_bank_transaction(transaction):
result = [] result = []
@@ -183,8 +192,8 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled" status = "Pending" if transaction["pending"] == "True" else "Settled"
tags = []
try: try:
tags = []
tags += transaction["category"] tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])] tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError: except KeyError:
@@ -217,6 +226,7 @@ def new_bank_transaction(transaction):
return result return result
def automatic_synchronization(): def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings") settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
@@ -224,4 +234,8 @@ def automatic_synchronization():
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts: for plaid_account in plaid_accounts:
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)

View File

@@ -1,14 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
import json import json
from frappe.utils.response import json_handler import unittest
import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype, add_account_type, add_bank_accounts,
new_bank_transaction, get_plaid_configuration)
from frappe.utils.response import json_handler
class TestPlaidSettings(unittest.TestCase): class TestPlaidSettings(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
def test_plaid_disabled(self): def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0) frappe.db.set_value("Plaid Settings", None, "enabled", 0)
self.assertTrue(plaid_configuration() == "disabled") self.assertTrue(get_plaid_configuration() == "disabled")
def test_add_account_type(self): def test_add_account_type(self):
add_account_type("brokerage") add_account_type("brokerage")
@@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000', 'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking' 'name': 'Plaid Checking'
}], }],
'institution': { 'institution': {
'institution_id': 'ins_6', 'institution_id': 'ins_6',
'name': 'Citi' 'name': 'Citi'
@@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000', 'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking' 'name': 'Plaid Checking'
}], }],
'institution': { 'institution': {
'institution_id': 'ins_6', 'institution_id': 'ins_6',
'name': 'Citi' 'name': 'Citi'

View File

@@ -1,3 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
@@ -31,7 +32,7 @@ class HolidayList(Document):
def validate_days(self): def validate_days(self):
if self.from_date > self.to_date: if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date")) throw(_("To Date cannot be before From Date"))
for day in self.get("holidays"): for day in self.get("holidays"):

View File

@@ -244,9 +244,10 @@
], ],
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"max_attachments": 3, "max_attachments": 3,
"modified": "2020-09-23 18:53:11.608446", "modified": "2020-09-23 19:11:58.806837",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Application", "name": "Leave Application",

View File

@@ -251,6 +251,10 @@ def make_bom(**args):
'rate': item_doc.valuation_rate or args.rate, 'rate': item_doc.valuation_rate or args.rate,
}) })
bom.insert(ignore_permissions=True) if not args.do_not_save:
bom.submit() bom.insert(ignore_permissions=True)
if not args.do_not_submit:
bom.submit()
return bom return bom

View File

@@ -371,6 +371,49 @@ class TestWorkOrder(unittest.TestCase):
ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
self.assertEqual(len(ste1.items), 3) self.assertEqual(len(ste1.items), 3)
def test_operation_time_with_batch_size(self):
fg_item = "Test Batch Size Item For BOM"
rm1 = "Test Batch Size Item RM 1 For BOM"
for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]:
make_item(item, {
"include_item_in_manufacturing": 1,
"is_stock_item": 1
})
bom_name = frappe.db.get_value("BOM",
{"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
if not bom_name:
bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
bom.with_operations = 1
bom.append("operations", {
"operation": "_Test Operation 1",
"workstation": "_Test Workstation 1",
"description": "Test Data",
"operating_cost": 100,
"time_in_mins": 40,
"batch_size": 5
})
bom.save()
bom.submit()
bom_name = bom.name
work_order = make_wo_order_test_record(item=fg_item,
planned_start_date=now(), qty=1, do_not_save=True)
work_order.set_work_order_operations()
work_order.save()
self.assertEqual(work_order.operations[0].time_in_mins, 8.0)
work_order1 = make_wo_order_test_record(item=fg_item,
planned_start_date=now(), qty=5, do_not_save=True)
work_order1.set_work_order_operations()
work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
def get_scrap_item_details(bom_no): def get_scrap_item_details(bom_no):
scrap_items = {} scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`

View File

@@ -611,7 +611,7 @@ erpnext.work_order = {
description: __('Max: {0}', [max]), description: __('Max: {0}', [max]),
default: max default: max
}, data => { }, data => {
max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
if (data.qty > max) { if (data.qty > max) {
frappe.msgprint(__('Quantity must not be more than {0}', [max])); frappe.msgprint(__('Quantity must not be more than {0}', [max]));

View File

@@ -364,7 +364,7 @@ class WorkOrder(Document):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
for d in self.get("operations"): for d in self.get("operations"):
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * math.ceil(flt(self.qty) / flt(d.batch_size)) d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost() self.calculate_operating_cost()

View File

@@ -4,17 +4,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings") frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
plaid_settings = frappe.get_single("Plaid Settings") plaid_settings = frappe.get_single("Plaid Settings")
if plaid_settings.enabled: if plaid_settings.enabled:
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \ if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret):
and frappe.conf.plaid_public_key and frappe.conf.plaid_secret):
plaid_settings.enabled = 0 plaid_settings.enabled = 0
else: else:
plaid_settings.update({ plaid_settings.update({
"plaid_client_id": frappe.conf.plaid_client_id, "plaid_client_id": frappe.conf.plaid_client_id,
"plaid_public_key": frappe.conf.plaid_public_key,
"plaid_env": frappe.conf.plaid_env, "plaid_env": frappe.conf.plaid_env,
"plaid_secret": frappe.conf.plaid_secret "plaid_secret": frappe.conf.plaid_secret
}) })

View File

@@ -14,13 +14,15 @@ def get_field_filter_data():
for f in fields: for f in fields:
doctype = f.get_link_doctype() doctype = f.get_link_doctype()
# apply enable/disable filter # apply enable/disable/show_in_website filter
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
filters = {} filters = {}
if meta.has_field('enabled'): if meta.has_field('enabled'):
filters['enabled'] = 1 filters['enabled'] = 1
if meta.has_field('disabled'): if meta.has_field('disabled'):
filters['disabled'] = 0 filters['disabled'] = 0
if meta.has_field('show_in_website'):
filters['show_in_website'] = 1
values = [d.name for d in frappe.get_all(doctype, filters)] values = [d.name for d in frappe.get_all(doctype, filters)]
filter_data.append([f, values]) filter_data.append([f, values])
@@ -378,7 +380,7 @@ def get_items(filters=None, search=None):
results = frappe.db.sql(''' results = frappe.db.sql('''
SELECT SELECT
`tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
`tabItem`.`website_image`, `tabItem`.`image`, `tabItem`.`website_image`, `tabItem`.`image`,
`tabItem`.`web_long_description`, `tabItem`.`description`, `tabItem`.`web_long_description`, `tabItem`.`description`,
`tabItem`.`route` `tabItem`.`route`

View File

@@ -672,25 +672,33 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
); );
} }
frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, let existing_amount = 0
['mode_of_payment', 'account', 'type'], (value) => { $.each(this.frm.doc.payments || [], function(i, row) {
if (this.frm.is_dirty()) { existing_amount += row.amount;
frappe.model.clear_table(this.frm.doc, 'payments'); })
if (value) {
let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
row.mode_of_payment = value.mode_of_payment;
row.type = value.type;
row.account = value.account;
row.default = 1;
row.amount = total_amount_to_pay;
} else {
this.frm.set_value('is_pos', 1);
}
this.frm.refresh_fields();
}
}, 'Sales Invoice');
this.calculate_paid_amount(); if (existing_amount != total_amount_to_pay) {
frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1},
['mode_of_payment', 'account', 'type'], (value) => {
if (this.frm.is_dirty()) {
frappe.model.clear_table(this.frm.doc, 'payments');
if (value) {
let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
row.mode_of_payment = value.mode_of_payment;
row.type = value.type;
row.account = value.account;
row.default = 1;
row.amount = total_amount_to_pay;
} else {
this.frm.set_value('is_pos', 1);
}
this.frm.refresh_fields();
this.calculate_paid_amount();
}
}, 'Sales Invoice');
} else {
this.calculate_paid_amount();
}
}, },
set_default_payment: function(total_amount_to_pay, update_paid_amount) { set_default_payment: function(total_amount_to_pay, update_paid_amount) {

View File

@@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) {
const frm = opts.frm; const frm = opts.frm;
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
this.data = []; this.data = [];
const fields = [{ const fields = [{
fieldtype:'Data', fieldtype:'Data',
@@ -472,14 +475,16 @@ erpnext.utils.update_child_items = function(opts) {
default: 0, default: 0,
read_only: 0, read_only: 0,
in_list_view: 1, in_list_view: 1,
label: __('Qty') label: __('Qty'),
precision: get_precision("qty")
}, { }, {
fieldtype:'Currency', fieldtype:'Currency',
fieldname:"rate", fieldname:"rate",
default: 0, default: 0,
read_only: 0, read_only: 0,
in_list_view: 1, in_list_view: 1,
label: __('Rate') label: __('Rate'),
precision: get_precision("rate")
}]; }];
if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
@@ -494,7 +499,8 @@ erpnext.utils.update_child_items = function(opts) {
fieldtype: 'Float', fieldtype: 'Float',
fieldname: "conversion_factor", fieldname: "conversion_factor",
in_list_view: 1, in_list_view: 1,
label: __("Conversion Factor") label: __("Conversion Factor"),
precision: get_precision('conversion_factor')
}) })
} }

View File

@@ -10,5 +10,13 @@ frappe.ui.form.on('Quality Procedure', {
} }
}; };
}); });
frm.set_query('parent_quality_procedure', function(){
return {
filters: {
is_group: 1
}
};
});
} }
}); });

View File

@@ -21,8 +21,7 @@
"fieldname": "parent_quality_procedure", "fieldname": "parent_quality_procedure",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Parent Procedure", "label": "Parent Procedure",
"options": "Quality Procedure", "options": "Quality Procedure"
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@@ -73,7 +72,7 @@
], ],
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-06-17 17:25:03.434953", "modified": "2020-10-12 16:14:11.167537",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Quality Management", "module": "Quality Management",
"name": "Quality Procedure", "name": "Quality Procedure",

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet, rebuild_tree
from frappe import _ from frappe import _
class QualityProcedure(NestedSet): class QualityProcedure(NestedSet):
@@ -42,6 +42,8 @@ class QualityProcedure(NestedSet):
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
def set_parent(self): def set_parent(self):
rebuild_tree('Quality Procedure', 'parent_quality_procedure')
for process in self.processes: for process in self.processes:
# Set parent for only those children who don't have a parent # Set parent for only those children who don't have a parent
parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure")

View File

@@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"client", "client",
"account_number_length",
"column_break_2", "column_break_2",
"client_number", "client_number",
"section_break_4", "section_break_4",
@@ -57,9 +58,16 @@
{ {
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "4",
"fieldname": "account_number_length",
"fieldtype": "Int",
"label": "Account Number Length",
"reqd": 1
} }
], ],
"modified": "2020-04-15 12:59:57.786506", "modified": "2020-10-03 16:52:35.903867",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "DATEV Settings", "name": "DATEV Settings",

View File

@@ -158,7 +158,7 @@ class GSTR3BReport(Document):
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"]) self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"]) self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y") self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas", "Registered Regular"], reverse_charge="Y")
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2) self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
self.set_itc_details(itc_details) self.set_itc_details(itc_details)
@@ -197,7 +197,7 @@ class GSTR3BReport(Document):
if d["ty"] == 'ISRC': if d["ty"] == 'ISRC':
reverse_charge = "Y" reverse_charge = "Y"
itc_type = 'All Other ITC' itc_type = 'All Other ITC'
gst_category = ['Unregistered', 'Overseas'] gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
else: else:
reverse_charge = "N" reverse_charge = "N"
@@ -253,7 +253,7 @@ class GSTR3BReport(Document):
def get_total_taxable_value(self, doctype, reverse_charge): def get_total_taxable_value(self, doctype, reverse_charge):
return frappe._dict(frappe.db.sql(""" return frappe._dict(frappe.db.sql("""
select gst_category, sum(net_total) as total select gst_category, sum(base_net_total) as total
from `tab{doctype}` from `tab{doctype}`
where docstatus = 1 and month(posting_date) = %s where docstatus = 1 and month(posting_date) = %s
and year(posting_date) = %s and reverse_charge = %s and year(posting_date) = %s and reverse_charge = %s

View File

@@ -16,6 +16,7 @@ from csv import QUOTE_NONNUMERIC
import frappe import frappe
from frappe import _ from frappe import _
from erpnext.accounts.utils import get_fiscal_year
import pandas as pd import pandas as pd
@@ -30,20 +31,33 @@ def execute(filters=None):
def validate(filters): def validate(filters):
"""Make sure all mandatory filters and settings are present.""" """Make sure all mandatory filters and settings are present."""
if not filters.get('company'): company = filters.get('company')
if not company:
frappe.throw(_('<b>Company</b> is a mandatory filter.')) frappe.throw(_('<b>Company</b> is a mandatory filter.'))
if not filters.get('from_date'): from_date = filters.get('from_date')
if not from_date:
frappe.throw(_('<b>From Date</b> is a mandatory filter.')) frappe.throw(_('<b>From Date</b> is a mandatory filter.'))
if not filters.get('to_date'): to_date = filters.get('to_date')
if not to_date:
frappe.throw(_('<b>To Date</b> is a mandatory filter.')) frappe.throw(_('<b>To Date</b> is a mandatory filter.'))
validate_fiscal_year(from_date, to_date, company)
try: try:
frappe.get_doc('DATEV Settings', filters.get('company')) frappe.get_doc('DATEV Settings', filters.get('company'))
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company'))) frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company')))
def validate_fiscal_year(from_date, to_date, company):
from_fiscal_year = get_fiscal_year(date=from_date, company=company)
to_fiscal_year = get_fiscal_year(date=to_date, company=company)
if from_fiscal_year != to_fiscal_year:
frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date))
def get_columns(): def get_columns():
"""Return the list of columns that will be shown in query report.""" """Return the list of columns that will be shown in query report."""
columns = [ columns = [
@@ -231,9 +245,9 @@ def get_datev_csv(data, filters):
# L = Tax client number (Mandantennummer) # L = Tax client number (Mandantennummer)
frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "", frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "",
# M = Start of the fiscal year (Wirtschaftsjahresbeginn) # M = Start of the fiscal year (Wirtschaftsjahresbeginn)
frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"),
# N = Length of account numbers (Sachkontenlänge) # N = Length of account numbers (Sachkontenlänge)
"4", str(filters.get('account_number_length', 4)),
# O = Transaction batch start date (YYYYMMDD) # O = Transaction batch start date (YYYYMMDD)
frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"), frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"),
# P = Transaction batch end date (YYYYMMDD) # P = Transaction batch end date (YYYYMMDD)
@@ -507,6 +521,12 @@ def download_datev_csv(filters=None):
filters = json.loads(filters) filters = json.loads(filters)
validate(filters) validate(filters)
filters['account_number_length'] = frappe.get_value('DATEV Settings', filters.get('company'), 'account_number_length')
fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=filters.get('company'))
filters['fiscal_year_start'] = fiscal_year[1]
data = get_gl_entries(filters, as_dict=1) data = get_gl_entries(filters, as_dict=1)
frappe.response['result'] = get_datev_csv(data, filters) frappe.response['result'] = get_datev_csv(data, filters)

View File

@@ -129,7 +129,7 @@ class Customer(TransactionBase):
address = frappe.get_doc('Address', address_name.get('name')) address = frappe.get_doc('Address', address_name.get('name'))
if not address.has_link('Customer', self.name): if not address.has_link('Customer', self.name):
address.append('links', dict(link_doctype='Customer', link_name=self.name)) address.append('links', dict(link_doctype='Customer', link_name=self.name))
address.save() address.save(ignore_permissions=self.flags.ignore_permissions)
lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
@@ -147,7 +147,7 @@ class Customer(TransactionBase):
contact = frappe.get_doc('Contact', contact_name.get('name')) contact = frappe.get_doc('Contact', contact_name.get('name'))
if not contact.has_link('Customer', self.name): if not contact.has_link('Customer', self.name):
contact.append('links', dict(link_doctype='Customer', link_name=self.name)) contact.append('links', dict(link_doctype='Customer', link_name=self.name))
contact.save() contact.save(ignore_permissions=self.flags.ignore_permissions)
else: else:
lead.lead_name = lead.lead_name.lstrip().split(" ") lead.lead_name = lead.lead_name.lstrip().split(" ")

View File

@@ -108,6 +108,10 @@ class TestQuotation(unittest.TestCase):
sales_order.transaction_date = nowdate() sales_order.transaction_date = nowdate()
sales_order.insert() sales_order.insert()
# Remove any unknown taxes if applied
sales_order.set('taxes', [])
sales_order.save()
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date)) self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00) self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)

View File

@@ -169,7 +169,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
} }
// project // project
if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { if(flt(doc.per_delivered, 2) < 100) {
this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create')); this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create'));
} }

View File

@@ -88,6 +88,8 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(len(si.get("items")), 1) self.assertEqual(len(si.get("items")), 1)
si.insert() si.insert()
si.set('taxes', [])
si.save()
self.assertEqual(si.payment_schedule[0].payment_amount, 500.0) self.assertEqual(si.payment_schedule[0].payment_amount, 500.0)
self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date) self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date)
@@ -401,6 +403,22 @@ class TestSalesOrder(unittest.TestCase):
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
def test_update_child_with_precision(self):
from frappe.model.meta import get_field_precision
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate"))
make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency")
so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664)
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}])
update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload()
self.assertEqual(so.items[0].rate, 200.34669)
make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency")
def test_update_child_qty_rate_perm(self): def test_update_child_qty_rate_perm(self):
so = make_sales_order(item_code= "_Test Item", qty=4) so = make_sales_order(item_code= "_Test Item", qty=4)
@@ -425,9 +443,9 @@ class TestSalesOrder(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
workflow = make_sales_order_workflow() workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
frappe.set_user("Administrator")
apply_workflow(so, 'Approve') apply_workflow(so, 'Approve')
frappe.set_user("Administrator")
user = 'test@example.com' user = 'test@example.com'
test_user = frappe.get_doc('User', user) test_user = frappe.get_doc('User', user)
test_user.add_roles("Sales User", "Test Junior Approver") test_user.add_roles("Sales User", "Test Junior Approver")
@@ -474,6 +492,95 @@ class TestSalesOrder(unittest.TestCase):
so.reload() so.reload()
self.assertEqual(so.packed_items[0].qty, 4) self.assertEqual(so.packed_items[0].qty, 4)
def test_update_child_with_tax_template(self):
"""
Test Action: Create a SO with one item having its tax account head already in the SO.
Add the same item + new item with tax template via Update Items.
Expected result: First Item's tax row is updated. New tax row is added for second Item.
"""
if not frappe.db.exists("Item", "Test Item with Tax"):
make_item("Test Item with Tax", {
'is_stock_item': 1,
})
if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
frappe.get_doc({
'doctype': 'Item Tax Template',
'title': 'Test Update Items Template',
'company': '_Test Company',
'taxes': [
{
'tax_type': "_Test Account Service Tax - _TC",
'tax_rate': 10,
}
]
}).insert()
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", {
"item_tax_template": "Test Update Items Template",
"valid_from": nowdate()
})
new_item_with_tax.save()
tax_template = "_Test Account Excise Duty @ 10"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
item_doc.append("taxes", {
"item_tax_template": tax_template,
"valid_from": nowdate()
})
item_doc.save()
else:
# update valid from
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
where parent = %(item)s and item_tax_template = %(tax)s""",
{"item": item, "tax": tax_template})
so = make_sales_order(item_code=item, qty=1, do_not_save=1)
so.append("taxes", {
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Sales Taxes and Charges",
"rate": 10
})
so.insert()
so.submit()
self.assertEqual(so.taxes[0].tax_amount, 10)
self.assertEqual(so.taxes[0].total, 110)
old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse")
frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC")
items = json.dumps([
{'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name},
{'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO
{'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
])
update_child_qty_rate('Sales Order', items, so.name)
so.reload()
self.assertEqual(so.taxes[0].tax_amount, 40)
self.assertEqual(so.taxes[0].total, 440)
self.assertEqual(so.taxes[1].account_head, "_Test Account Service Tax - _TC")
self.assertEqual(so.taxes[1].tax_amount, 40)
self.assertEqual(so.taxes[1].total, 480)
# teardown
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
so.cancel()
so.delete()
new_item_with_tax.delete()
frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
def test_warehouse_user(self): def test_warehouse_user(self):
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
@@ -1010,7 +1117,7 @@ def make_sales_order_workflow():
"is_active": 1, "is_active": 1,
"send_email_alert": 0, "send_email_alert": 0,
}) })
workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) workflow.append('states', dict( state = 'Pending', allow_edit = 'Administrator' ))
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
workflow.append('transitions', dict( workflow.append('transitions', dict(
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,

View File

@@ -372,7 +372,7 @@ class Company(NestedSet):
@frappe.whitelist() @frappe.whitelist()
def enqueue_replace_abbr(company, old, new): def enqueue_replace_abbr(company, old, new):
kwargs = dict(company=company, old=old, new=new) kwargs = dict(queue='long', company=company, old=old, new=new)
frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs)

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cstr from frappe.utils import cstr, cint
from frappe import msgprint, throw, _ from frappe import msgprint, throw, _
from frappe.model.document import Document from frappe.model.document import Document
@@ -159,7 +159,7 @@ class NamingSeries(Document):
prefix = self.parse_naming_series() prefix = self.parse_naming_series()
self.insert_series(prefix) self.insert_series(prefix)
frappe.db.sql("update `tabSeries` set current = %s where name = %s", frappe.db.sql("update `tabSeries` set current = %s where name = %s",
(self.current_value, prefix)) (cint(self.current_value), prefix))
msgprint(_("Series Updated Successfully")) msgprint(_("Series Updated Successfully"))
else: else:
msgprint(_("Please select prefix first")) msgprint(_("Please select prefix first"))

View File

@@ -111,6 +111,7 @@ class Item(WebsiteGenerator):
self.synced_with_hub = 0 self.synced_with_hub = 0
self.validate_has_variants() self.validate_has_variants()
self.validate_attributes_in_variants()
self.validate_stock_exists_for_template_item() self.validate_stock_exists_for_template_item()
self.validate_attributes() self.validate_attributes()
self.validate_variant_attributes() self.validate_variant_attributes()
@@ -806,6 +807,76 @@ class Item(WebsiteGenerator):
if frappe.db.exists("Item", {"variant_of": self.name}): if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw(_("Item has variants.")) frappe.throw(_("Item has variants."))
def validate_attributes_in_variants(self):
if not self.has_variants or self.get("__islocal"):
return
old_doc = self.get_doc_before_save()
old_doc_attributes = set([attr.attribute for attr in old_doc.attributes])
own_attributes = [attr.attribute for attr in self.attributes]
# Check if old attributes were removed from the list
# Is old_attrs is a subset of new ones
# that means we need not check any changes
if old_doc_attributes.issubset(set(own_attributes)):
return
from collections import defaultdict
# get all item variants
items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})]
# get all deleted attributes
deleted_attribute = list(old_doc_attributes.difference(set(own_attributes)))
# fetch all attributes of these items
item_attributes = frappe.get_all(
"Item Variant Attribute",
filters={
"parent": ["in", items],
"attribute": ["in", deleted_attribute]
},
fields=["attribute", "parent"]
)
not_included = defaultdict(list)
for attr in item_attributes:
if attr["attribute"] not in own_attributes:
not_included[attr["parent"]].append(attr["attribute"])
if not len(not_included):
return
def body(docnames):
docnames.sort()
return "<br>".join(docnames)
def table_row(title, body):
return """<tr>
<td>{0}</td>
<td>{1}</td>
</tr>""".format(title, body)
rows = ''
for docname, attr_list in not_included.items():
link = "<a href='#Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
rows += table_row(link, body(attr_list))
error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
message = """
<div>{0}</div><br>
<table class="table">
<thead>
<td>{1}</td>
<td>{2}</td>
</thead>
{3}
</table>
""".format(error_description, _('Variant Items'), _('Attributes'), rows)
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True)
def validate_stock_exists_for_template_item(self): def validate_stock_exists_for_template_item(self):
if self.stock_ledger_created() and self._doc_before_save: if self.stock_ledger_created() and self._doc_before_save:
if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)
@@ -1069,8 +1140,7 @@ def invalidate_item_variants_cache_for_website(doc):
if item_code: if item_code:
item_cache = ItemVariantsCacheManager(item_code) item_cache = ItemVariantsCacheManager(item_code)
item_cache.clear_cache() item_cache.rebuild_cache()
def check_stock_uom_with_bin(item, stock_uom): def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):

View File

@@ -1,357 +1,97 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:attribute_name", "autoname": "field:attribute_name",
"beta": 0,
"creation": "2014-09-26 03:49:54.899170", "creation": "2014-09-26 03:49:54.899170",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0, "engine": "InnoDB",
"field_order": [
"attribute_name",
"numeric_values",
"section_break_4",
"from_range",
"increment",
"column_break_8",
"to_range",
"section_break_5",
"item_attribute_values"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attribute_name", "fieldname": "attribute_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Attribute Name", "label": "Attribute Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fieldname": "numeric_values", "fieldname": "numeric_values",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Numeric Values"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Numeric Values",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "numeric_values", "depends_on": "numeric_values",
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "from_range", "fieldname": "from_range",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "From Range"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "From Range",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "increment", "fieldname": "increment",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "Increment"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Increment",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "to_range", "fieldname": "to_range",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "To Range"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "To Range",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: !doc.numeric_values", "depends_on": "eval: !doc.numeric_values",
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "item_attribute_values", "fieldname": "item_attribute_values",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Item Attribute Values", "label": "Item Attribute Values",
"length": 0, "options": "Item Attribute Value"
"no_copy": 0,
"options": "Item Attribute Value",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-edit", "icon": "fa fa-edit",
"idx": 0, "index_web_pages_for_search": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2020-10-02 12:03:02.359202",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-01 13:17:46.524806",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Attribute", "name": "Item Attribute",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Item Manager", "role": "Item Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt
from erpnext.controllers.item_variant import (validate_is_incremental, from erpnext.controllers.item_variant import (validate_is_incremental,
validate_item_attribute_value, InvalidItemAttributeValueError) validate_item_attribute_value, InvalidItemAttributeValueError)
@@ -42,7 +43,7 @@ class ItemAttribute(Document):
if self.from_range is None or self.to_range is None: if self.from_range is None or self.to_range is None:
frappe.throw(_("Please specify from/to range")) frappe.throw(_("Please specify from/to range"))
elif self.from_range >= self.to_range: elif flt(self.from_range) >= flt(self.to_range):
frappe.throw(_("From Range has to be less than To Range")) frappe.throw(_("From Range has to be less than To Range"))
if not self.increment: if not self.increment:

View File

@@ -50,16 +50,18 @@ class ItemPrice(Document):
def check_duplicates(self): def check_duplicates(self):
conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s" conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s"
condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name)
for field in ['uom', 'valid_from', for field in ['uom', 'valid_from',
'valid_upto', 'packing_unit', 'customer', 'supplier']: 'valid_upto', 'packing_unit', 'customer', 'supplier']:
if self.get(field): if self.get(field):
conditions += " and {0} = %({1})s".format(field, field) conditions += " and {0} = %({1})s".format(field, field)
condition_data_dict[field] = self.get(field)
price_list_rate = frappe.db.sql(""" price_list_rate = frappe.db.sql("""
SELECT price_list_rate SELECT price_list_rate
FROM `tabItem Price` FROM `tabItem Price`
{conditions} """.format(conditions=conditions), self.as_dict()) {conditions} """.format(conditions=conditions), condition_data_dict)
if price_list_rate : if price_list_rate :
frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem) frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem)

View File

@@ -207,7 +207,7 @@ class PurchaseReceipt(BuyingController):
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = self.get_company_default("stock_received_but_not_billed")
cogs_account = self.get_company_default("default_expense_account") stock_rbnb_currency = get_account_currency(stock_rbnb)
landed_cost_entries = get_item_account_wise_additional_cost(self.name) landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
@@ -243,7 +243,6 @@ class PurchaseReceipt(BuyingController):
# stock received but not billed # stock received but not billed
if d.base_net_amount: if d.base_net_amount:
stock_rbnb_currency = get_account_currency(stock_rbnb)
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": stock_rbnb, "account": stock_rbnb,
"against": warehouse_account[d.warehouse]["account"], "against": warehouse_account[d.warehouse]["account"],
@@ -289,6 +288,7 @@ class PurchaseReceipt(BuyingController):
if self.is_return or flt(d.item_tax_amount): if self.is_return or flt(d.item_tax_amount):
loss_account = expenses_included_in_valuation loss_account = expenses_included_in_valuation
else: else:
cogs_account = self.get_company_default("default_expense_account")
loss_account = cogs_account loss_account = cogs_account
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({

View File

@@ -471,8 +471,7 @@ class TestPurchaseReceipt(unittest.TestCase):
"expected_value_after_useful_life": 10, "expected_value_after_useful_life": 10,
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 1, "frequency_of_depreciation": 1
"depreciation_start_date": frappe.utils.nowdate()
}) })
asset.submit() asset.submit()
@@ -614,9 +613,9 @@ class TestPurchaseReceipt(unittest.TestCase):
rm_items = [ rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
"qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name},
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
"qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"} "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}
] ]
rm_item_string = json.dumps(rm_items) rm_item_string = json.dumps(rm_items)

View File

@@ -24,6 +24,24 @@ frappe.ui.form.on('Stock Entry', {
} }
}); });
frm.set_query('source_warehouse_address', function() {
return {
filters: {
link_doctype: 'Warehouse',
link_name: frm.doc.from_warehouse
}
}
});
frm.set_query('target_warehouse_address', function() {
return {
filters: {
link_doctype: 'Warehouse',
link_name: frm.doc.to_warehouse
}
}
});
frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => { frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => {
if (r.sample_retention_warehouse) { if (r.sample_retention_warehouse) {
var filters = [ var filters = [
@@ -139,6 +157,7 @@ frappe.ui.form.on('Stock Entry', {
mr_item.item_code = item.item_code; mr_item.item_code = item.item_code;
mr_item.item_name = item.item_name; mr_item.item_name = item.item_name;
mr_item.uom = item.uom; mr_item.uom = item.uom;
mr_item.stock_uom = item.stock_uom;
mr_item.conversion_factor = item.conversion_factor; mr_item.conversion_factor = item.conversion_factor;
mr_item.item_group = item.item_group; mr_item.item_group = item.item_group;
mr_item.description = item.description; mr_item.description = item.description;

View File

@@ -844,6 +844,8 @@ class StockEntry(StockController):
frappe.throw(_("Posting date and posting time is mandatory")) frappe.throw(_("Posting date and posting time is mandatory"))
self.set_work_order_details() self.set_work_order_details()
self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
"backflush_raw_materials_based_on")
if self.bom_no: if self.bom_no:
@@ -857,14 +859,14 @@ class StockEntry(StockController):
item["to_warehouse"] = self.pro_doc.wip_warehouse item["to_warehouse"] = self.pro_doc.wip_warehouse
self.add_to_stock_entry_detail(item_dict) self.add_to_stock_entry_detail(item_dict)
elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") elif (self.work_order and (self.purpose == "Manufacture"
and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings", or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer
"backflush_raw_materials_based_on")== "Material Transferred for Manufacture"): and self.flags.backflush_based_on == "Material Transferred for Manufacture"):
self.get_transfered_raw_materials() self.get_transfered_raw_materials()
elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \ elif (self.work_order and (self.purpose == "Manufacture" or
frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \ self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM"
frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1: and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1):
self.get_unconsumed_raw_materials() self.get_unconsumed_raw_materials()
else: else:
if not self.fg_completed_qty: if not self.fg_completed_qty:
@@ -1108,7 +1110,6 @@ class StockEntry(StockController):
for d in backflushed_materials.get(item.item_code): for d in backflushed_materials.get(item.item_code):
if d.get(item.warehouse): if d.get(item.warehouse):
if (qty > req_qty): if (qty > req_qty):
qty = req_qty
qty-= d.get(item.warehouse) qty-= d.get(item.warehouse)
if qty > 0: if qty > 0:
@@ -1133,11 +1134,22 @@ class StockEntry(StockController):
""" """
item_dict = self.get_pro_order_required_items() item_dict = self.get_pro_order_required_items()
max_qty = flt(self.pro_doc.qty) max_qty = flt(self.pro_doc.qty)
allow_overproduction = False
overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))
to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty)
transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100)
if transfer_limit_qty >= to_transfer_qty:
allow_overproduction = True
for item, item_details in iteritems(item_dict): for item, item_details in iteritems(item_dict):
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
if desire_to_transfer <= pending_to_issue: if desire_to_transfer <= pending_to_issue or allow_overproduction:
item_dict[item]["qty"] = desire_to_transfer item_dict[item]["qty"] = desire_to_transfer
elif pending_to_issue > 0: elif pending_to_issue > 0:
item_dict[item]["qty"] = pending_to_issue item_dict[item]["qty"] = pending_to_issue
@@ -1255,8 +1267,9 @@ class StockEntry(StockController):
FROM FROM
`tabStock Entry Detail` sed, `tabStock Entry` se `tabStock Entry Detail` sed, `tabStock Entry` se
WHERE WHERE
(pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code) pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code
AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s AND pos.parent = se.purchase_order AND sed.docstatus = 1
AND se.name = sed.parent and se.purchase_order = %(po)s
), 0) ), 0)
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})

View File

@@ -109,6 +109,10 @@ frappe.ui.form.on("Stock Reconciliation", {
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
if (frm.doc.purpose == "Stock Reconciliation" && !d.serial_no) {
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
}
} }
}); });
} }
@@ -183,6 +187,11 @@ frappe.ui.form.on("Stock Reconciliation Item", {
frappe.model.set_value(cdt, cdn, "batch_no", ""); frappe.model.set_value(cdt, cdn, "batch_no", "");
} }
if (child.serial_no) {
frappe.model.set_value(cdt, cdn, "serial_no", "");
frappe.model.set_value(cdt, cdn, "current_serial_no", "");
}
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}, },

View File

@@ -68,6 +68,8 @@ class StockReconciliation(StockController):
if item_dict.get("serial_nos"): if item_dict.get("serial_nos"):
item.current_serial_no = item_dict.get("serial_nos") item.current_serial_no = item_dict.get("serial_nos")
if self.purpose == "Stock Reconciliation" and not item.serial_no:
item.serial_no = item.current_serial_no
item.current_qty = item_dict.get("qty") item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate") item.current_valuation_rate = item_dict.get("rate")
@@ -172,8 +174,9 @@ class StockReconciliation(StockController):
row.serial_no = '' row.serial_no = ''
# item managed batch-wise not allowed # item managed batch-wise not allowed
if item.has_batch_no and not row.batch_no and not item.create_new_batch: if item.has_batch_no and not row.batch_no and not frappe.flags.in_test:
raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) if not item.create_new_batch or self.purpose != 'Opening Stock':
raise frappe.ValidationError(_("Batch no is required for the batched item {0}").format(item_code))
# docstatus should be < 2 # docstatus should be < 2
validate_cancelled_item(item_code, item.docstatus, verbose=0) validate_cancelled_item(item_code, item.docstatus, verbose=0)
@@ -191,10 +194,11 @@ class StockReconciliation(StockController):
serialized_items = False serialized_items = False
for row in self.items: for row in self.items:
item = frappe.get_cached_doc("Item", row.item_code) item = frappe.get_cached_doc("Item", row.item_code)
if not (item.has_serial_no or item.has_batch_no): if not (item.has_serial_no):
if row.serial_no or row.batch_no: if row.serial_no:
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
.format(row.idx, frappe.bold(row.item_code))) .format(row.idx, frappe.bold(row.item_code)))
previous_sle = get_previous_sle({ previous_sle = get_previous_sle({
"item_code": row.item_code, "item_code": row.item_code,
"warehouse": row.warehouse, "warehouse": row.warehouse,
@@ -217,7 +221,12 @@ class StockReconciliation(StockController):
or (not previous_sle and not row.qty)): or (not previous_sle and not row.qty)):
continue continue
sl_entries.append(self.get_sle_for_items(row)) sle_data = self.get_sle_for_items(row)
if row.batch_no:
sle_data.actual_qty = row.quantity_difference
sl_entries.append(sle_data)
else: else:
serialized_items = True serialized_items = True
@@ -244,7 +253,7 @@ class StockReconciliation(StockController):
serial_nos = get_serial_nos(row.serial_no) or [] serial_nos = get_serial_nos(row.serial_no) or []
# To issue existing serial nos # To issue existing serial nos
if row.current_qty and (row.current_serial_no or row.batch_no): if row.current_qty and (row.current_serial_no):
args = self.get_sle_for_items(row) args = self.get_sle_for_items(row)
args.update({ args.update({
'actual_qty': -1 * row.current_qty, 'actual_qty': -1 * row.current_qty,

View File

@@ -131,7 +131,7 @@ class TestStockReconciliation(unittest.TestCase):
to_delete_records.append(sr.name) to_delete_records.append(sr.name)
sr = create_stock_reconciliation(item_code=serial_item_code, sr = create_stock_reconciliation(item_code=serial_item_code,
warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos)) warehouse = serial_warehouse, qty=5, rate=300)
# print(sr.name) # print(sr.name)
serial_nos1 = get_serial_nos(sr.items[0].serial_no) serial_nos1 = get_serial_nos(sr.items[0].serial_no)
@@ -361,6 +361,37 @@ class TestStockReconciliation(unittest.TestCase):
doc.cancel() doc.cancel()
frappe.delete_doc(doc.doctype, doc.name) frappe.delete_doc(doc.doctype, doc.name)
def test_allow_negative_for_batch(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = "Stock-Reco-batch-Item-5"
warehouse = "_Test Warehouse for Stock Reco5 - _TC"
create_warehouse("_Test Warehouse for Stock Reco5", {"is_group": 0,
"parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"})
batch_item_doc = create_item(item_code, is_stock_item=1)
if not batch_item_doc.has_batch_no:
frappe.db.set_value("Item", item_code, {
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "Test-C.####"
})
ste1=make_stock_entry(posting_date="2020-10-07", posting_time="02:00", item_code=item_code,
target=warehouse, qty=2, basic_rate=100)
batch_no = ste1.items[0].batch_no
ste2=make_stock_entry(posting_date="2020-10-09", posting_time="02:00", item_code=item_code,
source=warehouse, qty=2, basic_rate=100, batch_no=batch_no)
sr = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, batch_no=batch_no, rate=200)
for doc in [sr, ste2, ste1]:
doc.cancel()
frappe.delete_doc(doc.doctype, doc.name)
def insert_existing_sle(warehouse): def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

View File

@@ -385,6 +385,11 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):
else: else:
warehouse = args.get('warehouse') warehouse = args.get('warehouse')
if not warehouse:
default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company:
return default_warehouse
return warehouse return warehouse
def update_barcode_value(out): def update_barcode_value(out):

View File

@@ -3,19 +3,70 @@
frappe.query_reports["Batch-Wise Balance History"] = { frappe.query_reports["Batch-Wise Balance History"] = {
"filters": [ "filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{ {
"fieldname":"from_date", "fieldname":"from_date",
"label": __("From Date"), "label": __("From Date"),
"fieldtype": "Date", "fieldtype": "Date",
"width": "80", "width": "80",
"default": frappe.sys_defaults.year_start_date, "default": frappe.sys_defaults.year_start_date,
"reqd": 1
}, },
{ {
"fieldname":"to_date", "fieldname":"to_date",
"label": __("To Date"), "label": __("To Date"),
"fieldtype": "Date", "fieldtype": "Date",
"width": "80", "width": "80",
"default": frappe.datetime.get_today() "default": frappe.datetime.get_today(),
} "reqd": 1
},
{
"fieldname":"item_code",
"label": __("Item Code"),
"fieldtype": "Link",
"options": "Item",
"get_query": function() {
return {
filters: {
"has_batch_no": 1
}
}
}
},
{
"fieldname":"warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse",
"get_query": function() {
let company = frappe.query_report.get_filter_value('company');
return {
filters: {
"company": company
}
}
}
},
{
"fieldname":"batch_no",
"label": __("Batch No"),
"fieldtype": "Link",
"options": "Batch",
"get_query": function() {
let item_code = frappe.query_report.get_filter_value('item_code');
return {
filters: {
"item": item_code
}
}
}
},
] ]
} }

View File

@@ -9,6 +9,9 @@ from frappe.utils import flt, cint, getdate
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
float_precision = cint(frappe.db.get_default("float_precision")) or 3 float_precision = cint(frappe.db.get_default("float_precision")) or 3
columns = get_columns(filters) columns = get_columns(filters)
@@ -50,6 +53,10 @@ def get_conditions(filters):
else: else:
frappe.throw(_("'To Date' is required")) frappe.throw(_("'To Date' is required"))
for field in ["item_code", "warehouse", "batch_no", "company"]:
if filters.get(field):
conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field)))
return conditions return conditions
#get all details #get all details

View File

@@ -77,38 +77,33 @@ def get_price_list():
return item_rate_map return item_rate_map
def get_last_purchase_rate(): def get_last_purchase_rate():
item_last_purchase_rate_map = {} item_last_purchase_rate_map = {}
query = """select * from (select query = """select * from (
result.item_code, (select
result.base_rate po_item.item_code,
from ( po.transaction_date as posting_date,
(select po_item.base_rate
po_item.item_code, from `tabPurchase Order` po, `tabPurchase Order Item` po_item
po_item.item_name, where po.name = po_item.parent and po.docstatus = 1)
po.transaction_date as posting_date, union
po_item.base_price_list_rate, (select
po_item.discount_percentage, pr_item.item_code,
po_item.base_rate pr.posting_date,
from `tabPurchase Order` po, `tabPurchase Order Item` po_item pr_item.base_rate
where po.name = po_item.parent and po.docstatus = 1) from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
union where pr.name = pr_item.parent and pr.docstatus = 1)
(select union
pr_item.item_code, (select
pr_item.item_name, pi_item.item_code,
pr.posting_date, pi.posting_date,
pr_item.base_price_list_rate, pi_item.base_rate
pr_item.discount_percentage, from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item
pr_item.base_rate where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1)
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item ) result order by result.item_code asc, result.posting_date asc"""
where pr.name = pr_item.parent and pr.docstatus = 1)
) result
order by result.item_code asc, result.posting_date desc) result_wrapper
group by item_code"""
for d in frappe.db.sql(query, as_dict=1): for d in frappe.db.sql(query, as_dict=1):
item_last_purchase_rate_map.setdefault(d.item_code, d.base_rate) item_last_purchase_rate_map[d.item_code] = d.base_rate
return item_last_purchase_rate_map return item_last_purchase_rate_map

View File

@@ -17,14 +17,17 @@ def execute(filters=None):
data = [] data = []
for item, item_dict in iteritems(item_details): for item, item_dict in iteritems(item_details):
earliest_age, latest_age = 0, 0
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
details = item_dict["details"] details = item_dict["details"]
if not fifo_queue or (not item_dict.get("total_qty")): continue if not fifo_queue and (not item_dict.get("total_qty")): continue
average_age = get_average_age(fifo_queue, to_date) average_age = get_average_age(fifo_queue, to_date)
earliest_age = date_diff(to_date, fifo_queue[0][1])
latest_age = date_diff(to_date, fifo_queue[-1][1]) if fifo_queue:
earliest_age = date_diff(to_date, fifo_queue[0][1])
latest_age = date_diff(to_date, fifo_queue[-1][1])
row = [details.name, details.item_name, row = [details.name, details.item_name,
details.description, details.item_group, details.brand] details.description, details.item_group, details.brand]
@@ -147,7 +150,8 @@ def get_fifo_queue(filters, sle=None):
item_details.setdefault(key, {"details": d, "fifo_queue": []}) item_details.setdefault(key, {"details": d, "fifo_queue": []})
fifo_queue = item_details[key]["fifo_queue"] fifo_queue = item_details[key]["fifo_queue"]
transferred_item_details.setdefault((d.voucher_no, d.name), []) transferred_item_key = (d.voucher_no, d.name, d.warehouse)
transferred_item_details.setdefault(transferred_item_key, [])
if d.voucher_type == "Stock Reconciliation": if d.voucher_type == "Stock Reconciliation":
d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0)) d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0))
@@ -155,10 +159,10 @@ def get_fifo_queue(filters, sle=None):
serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else [] serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else []
if d.actual_qty > 0: if d.actual_qty > 0:
if transferred_item_details.get((d.voucher_no, d.name)): if transferred_item_details.get(transferred_item_key):
batch = transferred_item_details[(d.voucher_no, d.name)][0] batch = transferred_item_details[transferred_item_key][0]
fifo_queue.append(batch) fifo_queue.append(batch)
transferred_item_details[((d.voucher_no, d.name))].pop(0) transferred_item_details[transferred_item_key].pop(0)
else: else:
if serial_no_list: if serial_no_list:
for serial_no in serial_no_list: for serial_no in serial_no_list:
@@ -182,11 +186,11 @@ def get_fifo_queue(filters, sle=None):
# if batch qty > 0 # if batch qty > 0
# not enough or exactly same qty in current batch, clear batch # not enough or exactly same qty in current batch, clear batch
qty_to_pop -= flt(batch[0]) qty_to_pop -= flt(batch[0])
transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) transferred_item_details[transferred_item_key].append(fifo_queue.pop(0))
else: else:
# all from current batch # all from current batch
batch[0] = flt(batch[0]) - qty_to_pop batch[0] = flt(batch[0]) - qty_to_pop
transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]]) transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]])
qty_to_pop = 0 qty_to_pop = 0
item_details[key]["qty_after_transaction"] = d.qty_after_transaction item_details[key]["qty_after_transaction"] = d.qty_after_transaction

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, flt from frappe.utils import cint, flt
from erpnext.stock.utils import update_included_uom_in_report from erpnext.stock.utils import update_included_uom_in_report
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute(filters=None): def execute(filters=None):
include_uom = filters.get("include_uom") include_uom = filters.get("include_uom")
@@ -23,6 +24,7 @@ def execute(filters=None):
actual_qty = stock_value = 0 actual_qty = stock_value = 0
available_serial_nos = {}
for sle in sl_entries: for sle in sl_entries:
item_detail = item_details[sle.item_code] item_detail = item_details[sle.item_code]
@@ -41,6 +43,9 @@ def execute(filters=None):
"stock_value": stock_value "stock_value": stock_value
}) })
if sle.serial_no:
update_available_serial_nos(available_serial_nos, sle)
data.append(sle) data.append(sle)
if include_uom: if include_uom:
@@ -49,6 +54,27 @@ def execute(filters=None):
update_included_uom_in_report(columns, data, include_uom, conversion_factors) update_included_uom_in_report(columns, data, include_uom, conversion_factors)
return columns, data return columns, data
def update_available_serial_nos(available_serial_nos, sle):
serial_nos = get_serial_nos(sle.serial_no)
key = (sle.item_code, sle.warehouse)
if key not in available_serial_nos:
available_serial_nos.setdefault(key, [])
existing_serial_no = available_serial_nos[key]
for sn in serial_nos:
if sle.actual_qty > 0:
if sn in existing_serial_no:
existing_serial_no.remove(sn)
else:
existing_serial_no.append(sn)
else:
if sn in existing_serial_no:
existing_serial_no.remove(sn)
else:
existing_serial_no.append(sn)
sle.balance_serial_no = '\n'.join(existing_serial_no)
def get_columns(): def get_columns():
columns = [ columns = [
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95}, {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95},
@@ -70,7 +96,8 @@ def get_columns():
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
{"label": _("Serial #"), "fieldname": "serial_no", "width": 100}, {"label": _("Serial No"), "fieldname": "serial_no", "width": 100},
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
{"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110}
] ]

View File

@@ -162,10 +162,13 @@ class update_entries_after(object):
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
else: else:
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: if sle.voucher_type=="Stock Reconciliation":
# assert if sle.batch_no:
self.qty_after_transaction += flt(sle.actual_qty)
else:
self.qty_after_transaction = sle.qty_after_transaction
self.valuation_rate = sle.valuation_rate self.valuation_rate = sle.valuation_rate
self.qty_after_transaction = sle.qty_after_transaction
self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]] self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
else: else:

View File

@@ -3,7 +3,7 @@ frappe
gocardless-pro==1.11.0 gocardless-pro==1.11.0
googlemaps==3.1.1 googlemaps==3.1.1
pandas==0.24.2 pandas==0.24.2
plaid-python==3.4.0 plaid-python==6.0.0
PyGithub==1.44.1 PyGithub==1.44.1
python-stdnum==1.12 python-stdnum==1.12
Unidecode==1.1.1 Unidecode==1.1.1