Merge branch 'version-13-beta-pre-release' of https://github.com/frappe/erpnext into add_account_method_v13

This commit is contained in:
Deepesh Garg
2021-03-16 13:24:33 +05:30
464 changed files with 15286 additions and 4797 deletions

View File

@@ -21,8 +21,8 @@ def docs_link_exists(body):
if word.startswith('http') and uri_validator(word): if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word) parsed_url = urlparse(word)
if parsed_url.netloc == "github.com": if parsed_url.netloc == "github.com":
_, org, repo, _type, ref = parsed_url.path.split('/') parts = parsed_url.path.split('/')
if org == "frappe" and repo in docs_repos: if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True return True

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__ = '13.0.0-beta.7' __version__ = '13.0.0-beta.13'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''
@@ -132,16 +132,10 @@ def allow_regional(fn):
return caller return caller
def get_last_membership(): def get_last_membership(member):
'''Returns last membership if exists''' '''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1) dict(member=member, paid=1), order_by='to_date desc', limit=1)
return last_membership and last_membership[0] if last_membership:
return last_membership[0]
def is_member():
'''Returns true if the user is still a member'''
last_membership = get_last_membership()
if last_membership and getdate(last_membership.to_date) > getdate():
return True
return False

View File

@@ -910,98 +910,8 @@
}, },
"is_group": 1 "is_group": 1
}, },
"Passiva": { "Passiva - Verbindlichkeiten": {
"root_type": "Liability", "root_type": "Liability",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_type": "Equity",
"account_number": "2900"
},
"Ausstehende Einlagen auf das gezeichnete Kapital": {
"account_number": "2910",
"is_group": 1
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
},
"B - R\u00fcckstellungen": { "B - R\u00fcckstellungen": {
"is_group": 1, "is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": { "1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@@ -1618,6 +1528,143 @@
}, },
"is_group": 1 "is_group": 1
}, },
"Passiva - Eigenkapital": {
"root_type": "Equity",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_number": "2900",
"account_type": "Equity"
},
"Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
"account_number": "2901"
},
"Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
"account_number": "2902"
},
"Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
"account_number": "2903"
},
"R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2906"
},
"Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2907"
},
"Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
"account_number": "2908"
},
"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
"account_number": "2910"
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
},
"Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
"account_number": "2925"
},
"Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
"account_number": "2926"
},
"Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
"account_number": "2927"
},
"Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
"account_number": "2928"
},
"Nachschusskapital (Gegenkonto 1299)": {
"account_number": "2929"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1,
"R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_number": "2935"
}
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Andere Gewinnr\u00fccklagen": {
"account_number": "2960"
},
"Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
"account_number": "2961"
},
"Eigenkapitalanteil von Wertaufholungen": {
"account_number": "2962"
},
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
}
},
"1 - Umsatzerl\u00f6se": { "1 - Umsatzerl\u00f6se": {
"root_type": "Income", "root_type": "Income",
"is_group": 1, "is_group": 1,

View File

@@ -254,7 +254,8 @@ def create_account(**kwargs):
account_name = kwargs.get('account_name'), account_name = kwargs.get('account_name'),
account_type = kwargs.get('account_type'), account_type = kwargs.get('account_type'),
parent_account = kwargs.get('parent_account'), parent_account = kwargs.get('parent_account'),
company = kwargs.get('company') company = kwargs.get('company'),
account_currency = kwargs.get('account_currency')
)) ))
account.save() account.save()

View File

@@ -2,7 +2,6 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension', { frappe.ui.form.on('Accounting Dimension', {
refresh: function(frm) { refresh: function(frm) {
frm.set_query('document_type', () => { frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list; let invalid_doctypes = frappe.model.core_doctypes_list;

View File

@@ -33,11 +33,11 @@ class AccountingDimension(Document):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self) frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long')
def on_trash(self): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self, queue='long')
else: else:
frappe.enqueue(delete_accounting_dimension, doc=self) frappe.enqueue(delete_accounting_dimension, doc=self)
@@ -48,6 +48,9 @@ class AccountingDimension(Document):
if not self.fieldname: if not self.fieldname:
self.fieldname = scrub(self.label) self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc): def make_dimension_in_accounting_doctypes(doc):
doclist = get_doctypes_with_dimensions() doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions()) doc_count = len(get_accounting_dimensions())
@@ -165,9 +168,9 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions(): def get_doctypes_with_dimensions():
doclist = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", doclist = ["GL Entry", "Sales Invoice", "POS Invoice", "Purchase Invoice", "Payment Entry", "Asset",
"Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
"Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
@@ -176,12 +179,14 @@ def get_doctypes_with_dimensions():
return doclist return doclist
def get_accounting_dimensions(as_list=True): def get_accounting_dimensions(as_list=True):
accounting_dimensions = frappe.get_all("Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"]) if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"])
if as_list: if as_list:
return [d.fieldname for d in accounting_dimensions] return [d.fieldname for d in frappe.flags.accounting_dimensions]
else: else:
return accounting_dimensions return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts(): def get_checks_for_pl_and_bs_accounts():
dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
@@ -203,7 +208,7 @@ def get_dimension_with_children(doctype, dimension):
return all_dimensions return all_dimensions
@frappe.whitelist() @frappe.whitelist()
def get_dimension_filters(): def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql(""" dimension_filters = frappe.db.sql("""
SELECT label, fieldname, document_type SELECT label, fieldname, document_type
FROM `tabAccounting Dimension` FROM `tabAccounting Dimension`
@@ -214,6 +219,18 @@ def get_dimension_filters():
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1) WHERE c.parent = p.name""", as_dict=1)
if with_cost_center_and_project:
dimension_filters.extend([
{
'fieldname': 'cost_center',
'document_type': 'Cost Center'
},
{
'fieldname': 'project',
'document_type': 'Project'
}
])
default_dimensions_map = {} default_dimensions_map = {}
for dimension in default_dimensions: for dimension in default_dimensions:
default_dimensions_map.setdefault(dimension.company, {}) default_dimensions_map.setdefault(dimension.company, {})

View File

@@ -11,37 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import d
class TestAccountingDimension(unittest.TestCase): class TestAccountingDimension(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.set_user("Administrator") create_dimension()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
dimension = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Department",
}).insert()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 0
dimension1.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Location",
})
dimension1.append("dimension_defaults", {
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1
})
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def test_dimension_against_sales_invoice(self): def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
@@ -101,6 +71,38 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self): def tearDown(self):
disable_dimension() disable_dimension()
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Department",
}).insert()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
dimension1 = frappe.get_doc({
"doctype": "Accounting Dimension",
"document_type": "Location",
})
dimension1.append("dimension_defaults", {
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1
})
dimension1.insert()
dimension1.save()
else:
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
dimension1.disabled = 0
dimension1.save()
def disable_dimension(): def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department") dimension1 = frappe.get_doc("Accounting Dimension", "Department")

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension Filter', {
refresh: function(frm, cdt, cdn) {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
let help_content =
`<table class="table table-bordered" style="background-color: #f9f9f9;">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
{{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
</p>
</td></tr>
</table>`;
frm.set_df_property('dimension_filter_help', 'options', help_content);
},
onload: function(frm) {
frm.set_query('applicable_on_account', 'accounts', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
frappe.db.get_list('Accounting Dimension',
{fields: ['document_type']}).then((res) => {
let options = ['Cost Center', 'Project'];
res.forEach((dimension) => {
options.push(dimension.document_type);
});
frm.set_df_property('accounting_dimension', 'options', options);
});
frm.trigger('setup_filters');
},
setup_filters: function(frm) {
let filters = {};
if (frm.doc.accounting_dimension) {
frappe.model.with_doctype(frm.doc.accounting_dimension, function() {
if (frappe.model.is_tree(frm.doc.accounting_dimension)) {
filters['is_group'] = 0;
}
if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) {
filters['company'] = frm.doc.company;
}
frm.set_query('dimension_value', 'dimensions', function() {
return {
filters: filters
};
});
});
}
},
accounting_dimension: function(frm) {
frm.clear_table("dimensions");
let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension;
frm.refresh_field("dimensions");
frm.trigger('setup_filters');
},
});
frappe.ui.form.on('Allowed Dimension', {
dimensions_add: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
row.accounting_dimension = frm.doc.accounting_dimension;
frm.refresh_field("dimensions");
}
});

View File

@@ -0,0 +1,158 @@
{
"actions": [],
"autoname": "format:{accounting_dimension}-{#####}",
"creation": "2020-11-08 18:28:11.906146",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"disabled",
"column_break_2",
"company",
"allow_or_restrict",
"section_break_4",
"accounts",
"column_break_6",
"dimensions",
"section_break_10",
"dimension_filter_help"
],
"fields": [
{
"fieldname": "accounting_dimension",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Accounting Dimension",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hide_border": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "allow_or_restrict",
"fieldtype": "Select",
"label": "Allow Or Restrict Dimension",
"options": "Allow\nRestrict",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Applicable On Account",
"options": "Applicable On Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.accounting_dimension",
"fieldname": "dimensions",
"fieldtype": "Table",
"label": "Applicable Dimension",
"options": "Allowed Dimension",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "dimension_filter_help",
"fieldtype": "HTML",
"label": "Dimension Filter Help",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-03 12:04:58.678402",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright, (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _, scrub
from frappe.model.document import Document
class AccountingDimensionFilter(Document):
def validate(self):
self.validate_applicable_accounts()
def validate_applicable_accounts(self):
accounts = frappe.db.sql(
"""
SELECT a.applicable_on_account as account
FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` d
WHERE d.name = a.parent
and d.name != %s
and d.accounting_dimension = %s
""", (self.name, self.accounting_dimension), as_dict=1)
account_list = [d.account for d in accounts]
for account in self.get('accounts'):
if account.applicable_on_account in account_list:
frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
def get_dimension_filter_map():
filters = frappe.db.sql("""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
FROM
`tabApplicable On Account` a, `tabAllowed Dimension` d,
`tabAccounting Dimension Filter` p
WHERE
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
""", as_dict=1)
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
f.allow_or_restrict, f.is_mandatory)
return dimension_filter_map
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
map_object.setdefault((dimension, account), {
'allowed_dimensions': [],
'is_mandatory': is_mandatory,
'allow_or_restrict': allow_or_restrict
})
map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)

View File

@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
create_dimension()
create_accounting_dimension_filter()
self.invoice_list = []
def test_allowed_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
si.items[0].cost_center = 'Main - _TC'
si.department = 'Accounts - _TC'
si.location = 'Block 1'
si.save()
self.assertRaises(InvalidAccountDimensionError, si.submit)
self.invoice_list.append(si)
def test_mandatory_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
si.department = ''
si.location = 'Block 1'
# Test with no department for Sales Account
si.items[0].department = ''
si.items[0].cost_center = '_Test Cost Center 2 - _TC'
si.save()
self.assertRaises(MandatoryAccountDimensionError, si.submit)
self.invoice_list.append(si)
def tearDown(self):
disable_dimension_filter()
disable_dimension()
for si in self.invoice_list:
si.load_from_db()
if si.docstatus == 1:
si.cancel()
def create_accounting_dimension_filter():
if not frappe.db.get_value('Accounting Dimension Filter',
{'accounting_dimension': 'Cost Center'}):
frappe.get_doc({
'doctype': 'Accounting Dimension Filter',
'accounting_dimension': 'Cost Center',
'allow_or_restrict': 'Allow',
'company': '_Test Company',
'accounts': [{
'applicable_on_account': 'Sales - _TC',
}],
'dimensions': [{
'accounting_dimension': 'Cost Center',
'dimension_value': '_Test Cost Center 2 - _TC'
}]
}).insert()
else:
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
doc.disabled = 0
doc.save()
if not frappe.db.get_value('Accounting Dimension Filter',
{'accounting_dimension': 'Department'}):
frappe.get_doc({
'doctype': 'Accounting Dimension Filter',
'accounting_dimension': 'Department',
'allow_or_restrict': 'Allow',
'company': '_Test Company',
'accounts': [{
'applicable_on_account': 'Sales - _TC',
'is_mandatory': 1
}],
'dimensions': [{
'accounting_dimension': 'Department',
'dimension_value': 'Accounts - _TC'
}]
}).insert()
else:
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
doc.disabled = 0
doc.save()
def disable_dimension_filter():
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
doc.disabled = 1
doc.save()
doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
doc.disabled = 1
doc.save()

View File

@@ -21,6 +21,7 @@
"book_asset_depreciation_entry_automatically", "book_asset_depreciation_entry_automatically",
"add_taxes_from_item_tax_template", "add_taxes_from_item_tax_template",
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"automatically_process_deferred_accounting_entry", "automatically_process_deferred_accounting_entry",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
@@ -219,6 +220,12 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Book Deferred Entries Based On", "label": "Book Deferred Entries Based On",
"options": "Days\nMonths" "options": "Days\nMonths"
},
{
"default": "0",
"fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -226,7 +233,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-10-13 11:32:52.268826", "modified": "2021-01-05 13:04:00.118892",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -0,0 +1,43 @@
{
"actions": [],
"creation": "2020-11-08 18:22:36.001131",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"dimension_value"
],
"fields": [
{
"fieldname": "accounting_dimension",
"fieldtype": "Link",
"label": "Accounting Dimension",
"options": "DocType",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "dimension_value",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"options": "accounting_dimension",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-23 09:56:19.744200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Allowed Dimension",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class AllowedDimension(Document):
pass

View File

@@ -0,0 +1,46 @@
{
"actions": [],
"creation": "2020-11-08 18:20:00.944449",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicable_on_account",
"is_mandatory"
],
"fields": [
{
"fieldname": "applicable_on_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Accounts",
"options": "Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"columns": 2,
"default": "0",
"fieldname": "is_mandatory",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-22 19:55:13.324136",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Applicable On Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class ApplicableOnAccount(Document):
pass

View File

@@ -1,5 +1,6 @@
// 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
frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', { frappe.ui.form.on('Bank', {
onload: function(frm) { onload: function(frm) {
@@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', {
frm.set_df_property('address_and_contact', 'hidden', 0); frm.set_df_property('address_and_contact', 'hidden', 0);
frappe.contacts.render_address_and_contact(frm); frappe.contacts.render_address_and_contact(frm);
} }
}, if (frm.doc.plaid_access_token) {
frm.add_custom_button(__('Refresh Plaid Link'), () => {
new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
});
}
}
}); });
@@ -41,3 +47,78 @@ let add_fields_to_mapping_table = function (frm) {
frm.fields_dict.bank_transaction_mapping.grid.refresh(); frm.fields_dict.bank_transaction_mapping.grid.refresh();
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
constructor(access_token) {
this.access_token = access_token;
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config();
}
async init_config() {
this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env');
this.token = await this.get_link_token_for_update();
this.init_plaid();
}
async get_link_token_for_update() {
const token = frappe.xcall(
'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update',
{ access_token: this.access_token }
)
if (!token) {
frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information'));
}
return token;
}
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
.then(() => {
me.onScriptLoaded(me);
})
.then(() => {
if (me.linkHandler) {
me.linkHandler.open();
}
})
.catch((error) => {
me.onScriptError(error);
});
}
loadScript(src) {
return new Promise(function (resolve, reject) {
if (document.querySelector("script[src='" + src + "']")) {
resolve();
return;
}
const el = document.createElement('script');
el.type = 'text/javascript';
el.async = true;
el.src = src;
el.addEventListener('load', resolve);
el.addEventListener('error', reject);
el.addEventListener('abort', reject);
document.head.appendChild(el);
});
}
onScriptLoaded(me) {
me.linkHandler = Plaid.create({
env: me.plaid_env,
token: me.token,
onSuccess: me.plaid_success
});
}
onScriptError(error) {
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
console.log(error);
}
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
}
};

View File

@@ -1,24 +1,9 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Budget', { frappe.ui.form.on('Budget', {
onload: function(frm) { onload: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("project", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("account", "accounts", function() { frm.set_query("account", "accounts", function() {
return { return {
filters: { filters: {
@@ -26,16 +11,18 @@ frappe.ui.form.on('Budget', {
report_type: "Profit and Loss", report_type: "Profit and Loss",
is_group: 0 is_group: 0
} }
} };
}) });
frm.set_query("monthly_distribution", function() { frm.set_query("monthly_distribution", function() {
return { return {
filters: { filters: {
fiscal_year: frm.doc.fiscal_year fiscal_year: frm.doc.fiscal_year
} }
} };
}) });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@@ -122,8 +122,10 @@ class TestBudget(unittest.TestCase):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -147,8 +149,11 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit) self.assertRaises(BudgetError, jv.submit)
@@ -159,10 +164,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@@ -181,12 +186,14 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
project=project)
self.assertTrue(frappe.db.get_value("GL Entry", self.assertTrue(frappe.db.get_value("GL Entry",
{"voucher_type": "Journal Entry", "voucher_no": jv.name})) {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
@@ -289,7 +296,7 @@ def make_budget(**args):
budget = frappe.new_doc("Budget") budget = frappe.new_doc("Budget")
if budget_against == "Project": if budget_against == "Project":
budget.project = "_Test Project" budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else: else:
budget.cost_center =cost_center or "_Test Cost Center - _TC" budget.cost_center =cost_center or "_Test Cost Center - _TC"

View File

@@ -11,8 +11,10 @@ from frappe.model.meta import get_field_precision
from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
from six import iteritems
exclude_from_linked_with = True exclude_from_linked_with = True
class GLEntry(Document): class GLEntry(Document):
@@ -25,29 +27,30 @@ class GLEntry(Document):
def validate(self): def validate(self):
self.flags.ignore_submit_comment = True self.flags.ignore_submit_comment = True
self.check_mandatory()
self.validate_and_set_fiscal_year() self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center() self.pl_must_have_cost_center()
self.validate_cost_center()
if not self.flags.from_repost: if not self.flags.from_repost:
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account() self.check_pl_account()
self.validate_party() self.validate_party()
self.validate_currency() self.validate_currency()
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False): def on_update(self):
if not from_repost: adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
self.validate_account_details(adv_adj) self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs() self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj) # Update outstanding amt on against voucher
validate_balance_type(self.account, adv_adj) if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
and self.against_voucher and self.flags.update_outstanding == 'Yes'):
# Update outstanding amt on against voucher update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \ self.against_voucher)
and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher)
def check_mandatory(self): def check_mandatory(self):
mandatory = ['account','voucher_type','voucher_no','company'] mandatory = ['account','voucher_type','voucher_no','company']
@@ -55,7 +58,7 @@ class GLEntry(Document):
if not self.get(k): if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
account_type = frappe.db.get_value("Account", self.account, "account_type") account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party): if not (self.party_type and self.party):
if account_type == "Receivable": if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}") frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
@@ -70,17 +73,15 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self): def pl_must_have_cost_center(self):
if frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss": if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher': if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.") frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
def validate_dimensions_for_pl_and_bs(self): def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type") account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts(): for dimension in get_checks_for_pl_and_bs_accounts():
if account_type == "Profit and Loss" \ if account_type == "Profit and Loss" \
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
if not self.get(dimension.fieldname): if not self.get(dimension.fieldname):
@@ -93,6 +94,25 @@ class GLEntry(Document):
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.") frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
.format(dimension.label, self.account)) .format(dimension.label, self.account))
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in iteritems(dimension_filter_map):
dimension = key[0]
account = key[1]
if self.account == account:
if value['is_mandatory'] and not self.get(dimension):
frappe.throw(_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
if value['allow_or_restrict'] == 'Allow':
if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
else:
if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \
@@ -120,26 +140,18 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account, self.company)) .format(self.voucher_type, self.voucher_no, self.account, self.company))
def validate_cost_center(self): def validate_cost_center(self):
if not hasattr(self, "cost_center_company"): if not self.cost_center: return
self.cost_center_company = {}
def _get_cost_center_company(): is_group, company = frappe.get_cached_value('Cost Center',
if not self.cost_center_company.get(self.cost_center): self.cost_center, ['is_group', 'company'])
self.cost_center_company[self.cost_center] = frappe.db.get_value(
"Cost Center", self.cost_center, "company")
return self.cost_center_company[self.cost_center] if company != self.company:
def _check_is_group():
return cint(frappe.get_cached_value('Cost Center', self.cost_center, 'is_group'))
if self.cost_center and _get_cost_center_company() != self.company:
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group(): if (self.voucher_type != 'Period Closing Voucher' and is_group):
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""") frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
.format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self): def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party) validate_party_frozen_disabled(self.party_type, self.party)
@@ -149,7 +161,7 @@ class GLEntry(Document):
account_currency = get_account_currency(self.account) account_currency = get_account_currency(self.account)
if not self.account_currency: if not self.account_currency:
self.account_currency = company_currency self.account_currency = account_currency or company_currency
if account_currency != self.account_currency: if account_currency != self.account_currency:
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")
@@ -163,7 +175,6 @@ class GLEntry(Document):
if not self.fiscal_year: if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
def validate_balance_type(account, adv_adj=False): def validate_balance_type(account, adv_adj=False):
if not adv_adj and account: if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
@@ -229,7 +240,7 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
def validate_frozen_account(account, adv_adj=None): def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.db.get_value("Account", account, "freeze_account") frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == 'Yes' and not adv_adj: if frozen_account == 'Yes' and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None, frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
'frozen_accounts_modifier') 'frozen_accounts_modifier')

View File

@@ -20,7 +20,8 @@ def get_data():
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
}, },
{ {
'items': ['Item'] 'label': _('Stock'),
'items': ['Item Groups', 'Item']
} }
] ]
} }

View File

@@ -120,6 +120,8 @@ frappe.ui.form.on("Journal Entry", {
} }
} }
}); });
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
voucher_type: function(frm){ voucher_type: function(frm){
@@ -197,6 +199,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
this.load_defaults(); this.load_defaults();
this.setup_queries(); this.setup_queries();
this.setup_balance_formatter(); this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
onload_post_render: function() { onload_post_render: function() {
@@ -222,15 +225,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
return erpnext.journal_entry.account_query(me.frm); return erpnext.journal_entry.account_query(me.frm);
}); });
me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) {
return {
filters: {
company: me.frm.doc.company,
is_group: 0
}
};
});
me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) { me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@@ -406,6 +400,8 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
} }
} }
cur_frm.cscript.update_totals(doc); cur_frm.cscript.update_totals(doc);
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts');
}, },
}); });

View File

@@ -299,15 +299,20 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_project(self): def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
project = make_project({
'project_name': 'Journal Entry Project', if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
'project_template_name': 'Test Project Template', project = make_project({
'start_date': '2020-01-01' 'project_name': 'Journal Entry Project',
}) 'project_template_name': 'Test Project Template',
'start_date': '2020-01-01'
})
project_name = project.name
else:
project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
for d in jv.accounts: for d in jv.accounts:
d.project = project.project_name d.project = project_name
jv.voucher_type = "Bank Entry" jv.voucher_type = "Bank Entry"
jv.multi_currency = 0 jv.multi_currency = 0
jv.cheque_no = "112233" jv.cheque_no = "112233"
@@ -317,10 +322,10 @@ class TestJournalEntry(unittest.TestCase):
expected_values = { expected_values = {
"_Test Cash - _TC": { "_Test Cash - _TC": {
"project": project.project_name "project": project_name
}, },
"_Test Bank - _TC": { "_Test Bank - _TC": {
"project": project.project_name "project": project_name
} }
} }

View File

@@ -1,6 +1,8 @@
// 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
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Loyalty Program', { frappe.ui.form.on('Loyalty Program', {
setup: function(frm) { setup: function(frm) {
var help_content = var help_content =
@@ -46,20 +48,17 @@ frappe.ui.form.on('Loyalty Program', {
}; };
}); });
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
};
});
frm.set_value("company", frappe.defaults.get_user_default("Company")); frm.set_value("company", frappe.defaults.get_user_default("Company"));
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) {
frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules.")); frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules."));
} }
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
} }
}); });

View File

@@ -36,6 +36,8 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
frm.page.set_indicator(__('In Progress'), 'orange'); frm.page.set_indicator(__('In Progress'), 'orange');
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
@@ -100,6 +102,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
} }
}) })
} }
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
invoice_type: function(frm) { invoice_type: function(frm) {

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
{% include "erpnext/public/js/controllers/accounts.js" %} {% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
@@ -8,6 +9,8 @@ frappe.ui.form.on('Payment Entry', {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
} }
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
setup: function(frm) { setup: function(frm) {
@@ -88,24 +91,17 @@ frappe.ui.form.on('Payment Entry', {
} }
}); });
frm.set_query("cost_center", "deductions", function() {
return {
filters: {
"is_group": 0,
"company": frm.doc.company
}
}
});
frm.set_query("reference_doctype", "references", function() { frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") { if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type=="Supplier") { } else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type=="Employee") { } else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"]; var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type=="Student") { } else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"]; var doctypes = ["Fees"];
} else if (frm.doc.party_type == "Donor") {
var doctypes = ["Donation"];
} else { } else {
var doctypes = ["Journal Entry"]; var doctypes = ["Journal Entry"];
} }
@@ -134,7 +130,7 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn]; const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company}; const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation'];
if (in_list(party_type_doctypes, child.reference_doctype)) { if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party; filters[doc.party_type.toLowerCase()] = doc.party;
@@ -167,6 +163,7 @@ frappe.ui.form.on('Payment Entry', {
company: function(frm) { company: function(frm) {
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
contact_person: function(frm) { contact_person: function(frm) {
@@ -286,7 +283,7 @@ frappe.ui.form.on('Payment Entry', {
let party_types = Object.keys(frappe.boot.party_account_types); let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){ if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("party_type", ""); frm.set_value("party_type", "");
frappe.throw(__("Party can only be one of "+ party_types.join(", "))); frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")]));
} }
frm.set_query("party", function() { frm.set_query("party", function() {
@@ -401,6 +398,8 @@ frappe.ui.form.on('Payment Entry', {
set_account_currency_and_balance: function(frm, account, currency_field, set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) { balance_field, callback_function) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) { if (frm.doc.posting_date && account) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@@ -427,6 +426,14 @@ frappe.ui.form.on('Payment Entry', {
if(!frm.doc.paid_amount && frm.doc.received_amount) if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm); frm.events.received_amount(frm);
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
&& frm.doc.paid_amount != frm.doc.received_amount) {
if (company_currency != frm.doc.paid_from_account_currency &&
frm.doc.payment_type == "Pay") {
frm.doc.paid_amount = frm.doc.received_amount;
}
}
} }
}, },
() => { () => {
@@ -700,7 +707,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) { ) {
if(total_positive_outstanding > total_negative_outstanding) if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount) if (!frm.doc.paid_amount)
@@ -743,7 +751,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) { ) {
if(total_positive_outstanding_including_order > paid_amount) { if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
@@ -900,6 +909,12 @@ frappe.ui.form.on('Payment Entry', {
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false; return false;
} }
if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") {
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx]));
return false;
}
} }
if (row) { if (row) {

View File

@@ -72,6 +72,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation()
self.update_payment_schedule() self.update_payment_schedule()
self.set_status() self.set_status()
@@ -82,6 +83,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation(cancel=1)
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1) self.update_payment_schedule(cancel=1)
self.set_payment_req_status() self.set_payment_req_status()
@@ -245,6 +247,8 @@ class PaymentEntry(AccountsController):
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance")
elif self.party_type == "Shareholder": elif self.party_type == "Shareholder":
valid_reference_doctypes = ("Journal Entry") valid_reference_doctypes = ("Journal Entry")
elif self.party_type == "Donor":
valid_reference_doctypes = ("Donation")
for d in self.get("references"): for d in self.get("references"):
if not d.allocated_amount: if not d.allocated_amount:
@@ -614,6 +618,13 @@ class PaymentEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name) doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.name) update_reimbursed_amount(doc, self.name)
def update_donation(self, cancel=0):
if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
for d in self.get("references"):
if d.reference_doctype=="Donation" and d.reference_name:
is_paid = 0 if cancel else 1
frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name self.reference_no = reference_doc.name
self.reference_date = nowdate() self.reference_date = nowdate()
@@ -913,6 +924,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total") total_amount = ref_doc.get("grand_total")
exchange_rate = 1 exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount") outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount")
exchange_rate = 1
elif reference_doctype == "Dunning": elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount") total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1 exchange_rate = 1
@@ -1162,8 +1176,10 @@ def set_party_type(dt):
party_type = "Supplier" party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"): elif dt in ("Expense Claim", "Employee Advance"):
party_type = "Employee" party_type = "Employee"
elif dt in ("Fees"): elif dt == "Fees":
party_type = "Student" party_type = "Student"
elif dt == "Donation":
party_type = "Donor"
return party_type return party_type
def set_party_account(dt, dn, doc, party_type): def set_party_account(dt, dn, doc, party_type):
@@ -1189,7 +1205,7 @@ def set_party_account_currency(dt, party_account, doc):
return party_account_currency return party_account_currency
def set_payment_type(dt, doc): def set_payment_type(dt, doc):
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive" payment_type = "Receive"
else: else:
@@ -1222,6 +1238,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
elif dt == "Dunning": elif dt == "Dunning":
grand_total = doc.grand_total grand_total = doc.grand_total
outstanding_amount = doc.grand_total outstanding_amount = doc.grand_total
elif dt == "Donation":
grand_total = doc.amount
outstanding_amount = doc.amount
else: else:
if party_account_currency == doc.company_currency: if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)

View File

@@ -3,6 +3,7 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -82,18 +83,37 @@ class PaymentRequest(Document):
self.make_communication_entry() self.make_communication_entry()
elif self.payment_channel == "Phone": elif self.payment_channel == "Phone":
controller = get_payment_gateway_controller(self.payment_gateway) self.request_phone_payment()
payment_record = dict(
reference_doctype="Payment Request", def request_phone_payment(self):
reference_docname=self.name, controller = get_payment_gateway_controller(self.payment_gateway)
payment_reference=self.reference_name, request_amount = self.get_request_amount()
grand_total=self.grand_total,
sender=self.email_to, payment_record = dict(
currency=self.currency, reference_doctype="Payment Request",
payment_gateway=self.payment_gateway reference_docname=self.name,
) payment_reference=self.reference_name,
controller.validate_transaction_currency(self.currency) request_amount=request_amount,
controller.request_for_payment(**payment_record) sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def get_request_amount(self):
data_of_completed_requests = frappe.get_all("Integration Request", filters={
'reference_doctype': self.doctype,
'reference_docname': self.name,
'status': 'Completed'
}, pluck="data")
if not data_of_completed_requests:
return self.grand_total
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
return request_amounts
def on_cancel(self): def on_cancel(self):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
@@ -351,8 +371,8 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart" or args.mute_email: if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True pr.flags.mute_email = True
pr.insert(ignore_permissions=True)
if args.submit_doc: if args.submit_doc:
pr.insert(ignore_permissions=True)
pr.submit() pr.submit()
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
@@ -412,8 +432,8 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
def get_gateway_details(args): def get_gateway_details(args):
"""return gateway and payment account of default payment gateway""" """return gateway and payment account of default payment gateway"""
if args.get("payment_gateway"): if args.get("payment_gateway_account"):
return get_payment_gateway_account(args.get("payment_gateway")) return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account

View File

@@ -45,7 +45,8 @@ class TestPaymentRequest(unittest.TestCase):
def test_payment_request_linkings(self): def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com") pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - INR")
self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_doctype, "Sales Order")
self.assertEqual(pr.reference_name, so_inr.name) self.assertEqual(pr.reference_name, so_inr.name)
@@ -54,7 +55,8 @@ class TestPaymentRequest(unittest.TestCase):
conversion_rate = get_exchange_rate("USD", "INR") conversion_rate = get_exchange_rate("USD", "INR")
si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com") pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - USD")
self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_doctype, "Sales Invoice")
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
@@ -68,7 +70,7 @@ class TestPaymentRequest(unittest.TestCase):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
mute_email=1, submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1)
pe = pr.set_as_paid() pe = pr.set_as_paid()
so_inr = frappe.get_doc("Sales Order", so_inr.name) so_inr = frappe.get_doc("Sales Order", so_inr.name)
@@ -79,7 +81,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.set_as_paid() pe = pr.set_as_paid()
@@ -106,7 +108,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.create_payment_entry() pe = pr.create_payment_entry()
pr.load_from_db() pr.load_from_db()

View File

@@ -8,7 +8,7 @@ from frappe import _
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions, from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions,
get_dimension_filters) get_dimensions)
class PeriodClosingVoucher(AccountsController): class PeriodClosingVoucher(AccountsController):
def validate(self): def validate(self):
@@ -58,7 +58,7 @@ class PeriodClosingVoucher(AccountsController):
for dimension in accounting_dimensions: for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension)) dimension_fields.append('t1.{0}'.format(dimension))
dimension_filters, default_dimensions = get_dimension_filters() dimension_filters, default_dimensions = get_dimensions()
pl_accounts = self.get_pl_balances(dimension_fields) pl_accounts = self.get_pl_balances(dimension_fields)

View File

@@ -3,6 +3,7 @@
frappe.ui.form.on('POS Closing Entry', { frappe.ui.form.on('POS Closing Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
frm.set_query("pos_profile", function(doc) { frm.set_query("pos_profile", function(doc) {
return { return {
filters: { 'user': doc.user } filters: { 'user': doc.user }
@@ -20,7 +21,7 @@ frappe.ui.form.on('POS Closing Entry', {
return { filters: { 'status': 'Open', 'docstatus': 1 } }; return { filters: { 'status': 'Open', 'docstatus': 1 } };
}); });
if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime()); if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
if (frm.doc.docstatus === 1) set_html_data(frm); if (frm.doc.docstatus === 1) set_html_data(frm);
}, },

View File

@@ -11,6 +11,7 @@
"column_break_3", "column_break_3",
"posting_date", "posting_date",
"pos_opening_entry", "pos_opening_entry",
"status",
"section_break_5", "section_break_5",
"company", "company",
"column_break_7", "column_break_7",
@@ -184,11 +185,27 @@
"label": "POS Opening Entry", "label": "POS Opening Entry",
"options": "POS Opening Entry", "options": "POS Opening Entry",
"reqd": 1 "reqd": 1
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nSubmitted\nQueued\nCancelled",
"print_hide": 1,
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [
"modified": "2020-05-29 15:03:22.226113", {
"link_doctype": "POS Invoice Merge Log",
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2021-01-12 12:21:05.388650",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Closing Entry", "name": "POS Closing Entry",

View File

@@ -6,13 +6,12 @@ from __future__ import unicode_literals
import frappe import frappe
import json import json
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.utils import get_datetime, flt
from frappe.utils import getdate, get_datetime, flt from erpnext.controllers.status_updater import StatusUpdater
from collections import defaultdict
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices, unconsolidate_pos_invoices
class POSClosingEntry(Document): class POSClosingEntry(StatusUpdater):
def validate(self): def validate(self):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
@@ -57,21 +56,30 @@ class POSClosingEntry(Document):
if not invalid_rows: if not invalid_rows:
return return
error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows] error_list = []
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) for row in invalid_rows:
for msg in row.get('msg'):
error_list.append(_("Row #{}: {}").format(row.get('idx'), msg))
def on_submit(self): frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
merge_pos_invoices(self.pos_transactions)
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
opening_entry.pos_closing_entry = self.name
opening_entry.set_status()
opening_entry.save()
def get_payment_reconciliation_details(self): def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency") currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"data": self, "currency": currency}) {"data": self, "currency": currency})
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
def update_opening_entry(self, for_cancel=False):
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
opening_entry.pos_closing_entry = self.name if not for_cancel else None
opening_entry.set_status()
opening_entry.save()
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_cashiers(doctype, txt, searchfield, start, page_len, filters): def get_cashiers(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -0,0 +1,16 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['POS Closing Entry'] = {
get_indicator: function(doc) {
var status_color = {
"Draft": "red",
"Submitted": "blue",
"Queued": "orange",
"Cancelled": "red"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
}
};

View File

@@ -13,7 +13,6 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
class TestPOSClosingEntry(unittest.TestCase): class TestPOSClosingEntry(unittest.TestCase):
def test_pos_closing_entry(self): def test_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name) opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
@@ -45,6 +44,49 @@ class TestPOSClosingEntry(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
def test_cancelling_of_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
})
pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
})
pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
payment = pcv_doc.payment_reconciliation[0]
self.assertEqual(payment.mode_of_payment, 'Cash')
for d in pcv_doc.payment_reconciliation:
if d.mode_of_payment == 'Cash':
d.closing_amount = 6700
pcv_doc.submit()
pos_inv1.load_from_db()
self.assertRaises(frappe.ValidationError, pos_inv1.cancel)
si_doc = frappe.get_doc("Sales Invoice", pos_inv1.consolidated_invoice)
self.assertRaises(frappe.ValidationError, si_doc.cancel)
pcv_doc.load_from_db()
pcv_doc.cancel()
si_doc.load_from_db()
pos_inv1.load_from_db()
self.assertEqual(si_doc.docstatus, 2)
self.assertEqual(pos_inv1.status, 'Paid')
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
def init_user_and_profile(**args): def init_user_and_profile(**args):
user = 'test@example.com' user = 'test@example.com'
test_user = frappe.get_doc('User', user) test_user = frappe.get_doc('User', user)

View File

@@ -2,6 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
setup(doc) { setup(doc) {
@@ -9,12 +10,19 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
this._super(doc); this._super(doc);
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload(doc) { onload(doc) {
this._super(); this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos"); this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields(); this.frm.refresh_fields();
} }
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh(doc) { refresh(doc) {
@@ -187,18 +195,43 @@ frappe.ui.form.on('POS Invoice', {
}, },
request_for_payment: function (frm) { request_for_payment: function (frm) {
if (!frm.doc.contact_mobile) {
frappe.throw(__('Please enter mobile number first.'));
}
frm.dirty();
frm.save().then(() => { frm.save().then(() => {
frappe.dom.freeze(); frappe.dom.freeze(__('Waiting for payment...'));
frappe.call({ frappe
method: 'create_payment_request', .call({
doc: frm.doc, method: 'create_payment_request',
}) doc: frm.doc
})
.fail(() => { .fail(() => {
frappe.dom.unfreeze(); frappe.dom.unfreeze();
frappe.msgprint('Payment request failed'); frappe.msgprint(__('Payment request failed'));
}) })
.then(() => { .then(({ message }) => {
frappe.msgprint('Payment request sent successfully'); const payment_request_name = message.name;
setTimeout(() => {
frappe.db.get_value('Payment Request', payment_request_name, ['status', 'grand_total']).then(({ message }) => {
if (message.status != 'Paid') {
frappe.dom.unfreeze();
frappe.msgprint({
message: __('Payment Request took too long to respond. Please try requesting for payment again.'),
title: __('Request Timeout')
});
} else if (frappe.dom.freeze_count != 0) {
frappe.dom.unfreeze();
cur_frm.reload_doc();
cur_pos.payment.events.submit_invoice();
frappe.show_alert({
message: __("Payment of {0} received successfully.", [format_currency(message.grand_total, frm.doc.currency, 0)]),
indicator: 'green'
});
}
});
}, 60000);
}); });
}); });
} }

View File

@@ -6,10 +6,9 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.controllers.selling_controller import SellingController
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.utils import cint, flt, getdate, nowdate, get_link_to_form
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -59,6 +58,22 @@ class POSInvoice(SalesInvoice):
self.check_phone_payments() self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
def before_cancel(self):
if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
pos_closing_entry = frappe.get_all(
"POS Invoice Reference",
ignore_permissions=True,
filters={ 'pos_invoice': self.name },
pluck="parent",
limit=1
)
frappe.throw(
_('You need to cancel POS Closing Entry {} to be able to cancel this document.').format(
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
),
title=_('Not Allowed')
)
def on_cancel(self): def on_cancel(self):
# run on cancel method of selling controller # run on cancel method of selling controller
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
@@ -78,7 +93,7 @@ class POSInvoice(SalesInvoice):
mode_of_payment=pay.mode_of_payment, status="Paid"), mode_of_payment=pay.mode_of_payment, status="Paid"),
fieldname="grand_total") fieldname="grand_total")
if pay.amount != paid_amt: if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_stock_availablility(self): def validate_stock_availablility(self):
@@ -266,6 +281,8 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile: if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {} pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name') self.pos_profile = pos_profile.get('name')
profile = {} profile = {}
@@ -294,14 +311,21 @@ class POSInvoice(SalesInvoice):
self.set(fieldname, profile.get(fieldname)) self.set(fieldname, profile.get(fieldname))
if self.customer: if self.customer:
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) customer_price_list, customer_group, customer_currency = frappe.db.get_value(
"Customer", self.customer, ['default_price_list', 'customer_group', 'default_currency']
)
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list') selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
if customer_currency != profile.get('currency'):
self.set('currency', customer_currency)
else: else:
selling_price_list = profile.get('selling_price_list') selling_price_list = profile.get('selling_price_list')
if selling_price_list: if selling_price_list:
self.set('selling_price_list', selling_price_list) self.set('selling_price_list', selling_price_list)
if customer_currency != profile.get('currency'):
self.set('currency', customer_currency)
# set pos values in items # set pos values in items
for item in self.get("items"): for item in self.get("items"):
@@ -361,22 +385,48 @@ class POSInvoice(SalesInvoice):
if not self.contact_mobile: if not self.contact_mobile:
frappe.throw(_("Please enter the phone number first")) frappe.throw(_("Please enter the phone number first"))
payment_gateway = frappe.db.get_value("Payment Gateway Account", { pay_req = self.get_existing_payment_request(pay)
"payment_account": pay.account, if not pay_req:
}) pay_req = self.get_new_payment_request(pay)
record = { pay_req.submit()
"payment_gateway": payment_gateway, else:
"dt": "POS Invoice", pay_req.request_phone_payment()
"dn": self.name,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"mode_of_payment": pay.mode_of_payment,
"recipient_id": self.contact_mobile,
"submit_doc": True
}
return make_payment_request(**record) return pay_req
def get_new_payment_request(self, mop):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": mop.account,
}, ["name"])
args = {
"dt": "POS Invoice",
"dn": self.name,
"recipient_id": self.contact_mobile,
"mode_of_payment": mop.mode_of_payment,
"payment_gateway_account": payment_gateway_account,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"return_doc": True
}
return make_payment_request(**args)
def get_existing_payment_request(self, pay):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": pay.account,
}, ["name"])
args = {
'doctype': 'Payment Request',
'reference_doctype': 'POS Invoice',
'reference_name': self.name,
'payment_gateway_account': payment_gateway_account,
'email_to': self.contact_mobile
}
pr = frappe.db.exists(args)
if pr:
return frappe.get_doc('Payment Request', pr[0][0])
def add_return_modes(doc, pos_profile): def add_return_modes(doc, pos_profile):
def append_payment(payment_mode): def append_payment(payment_mode):

View File

@@ -290,7 +290,7 @@ class TestPOSInvoice(unittest.TestCase):
def test_merging_into_sales_invoice_with_discount(self): def test_merging_into_sales_invoice_with_discount(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
@@ -306,7 +306,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
pos_inv2.submit() pos_inv2.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@@ -315,7 +315,7 @@ class TestPOSInvoice(unittest.TestCase):
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
@@ -348,7 +348,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
pos_inv2.submit() pos_inv2.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@@ -357,7 +357,7 @@ class TestPOSInvoice(unittest.TestCase):
def test_merging_with_validate_selling_price(self): def test_merging_with_validate_selling_price(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1) frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
@@ -393,7 +393,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
pos_inv2.submit() pos_inv2.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv2.load_from_db() pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")

View File

@@ -7,6 +7,8 @@
"field_order": [ "field_order": [
"posting_date", "posting_date",
"customer", "customer",
"column_break_3",
"pos_closing_entry",
"section_break_3", "section_break_3",
"pos_invoices", "pos_invoices",
"references_section", "references_section",
@@ -76,11 +78,22 @@
"label": "Consolidated Credit Note", "label": "Consolidated Credit Note",
"options": "Sales Invoice", "options": "Sales Invoice",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_closing_entry",
"fieldtype": "Link",
"label": "POS Closing Entry",
"options": "POS Closing Entry"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:08:41.317100", "modified": "2020-12-01 11:53:57.267579",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Merge Log", "name": "POS Invoice Merge Log",

View File

@@ -5,10 +5,13 @@
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, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from frappe.model.document import Document
from frappe.model.mapper import map_doc
from frappe.model import default_fields from frappe.model import default_fields
from frappe.model.document import Document
from frappe.utils import flt, getdate, nowdate
from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info
from six import iteritems from six import iteritems
@@ -61,7 +64,13 @@ class POSInvoiceMergeLog(Document):
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(sales_invoice, credit_note) self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
def on_cancel(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
self.update_pos_invoices(pos_invoice_docs)
self.cancel_linked_invoices()
def process_merging_into_sales_invoice(self, data): def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice() sales_invoice = self.get_new_sales_invoice()
@@ -83,7 +92,7 @@ class POSInvoiceMergeLog(Document):
credit_note.is_consolidated = 1 credit_note.is_consolidated = 1
# TODO: return could be against multiple sales invoice which could also have been consolidated? # TODO: return could be against multiple sales invoice which could also have been consolidated?
credit_note.return_against = self.consolidated_invoice # credit_note.return_against = self.consolidated_invoice
credit_note.save() credit_note.save()
credit_note.submit() credit_note.submit()
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
@@ -111,7 +120,9 @@ class POSInvoiceMergeLog(Document):
i.qty = i.qty + item.qty i.qty = i.qty + item.qty
if not found: if not found:
item.rate = item.net_rate item.rate = item.net_rate
items.append(item) item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
for tax in doc.get('taxes'): for tax in doc.get('taxes'):
found = False found = False
@@ -147,6 +158,8 @@ class POSInvoiceMergeLog(Document):
invoice.set('taxes', taxes) invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0 invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0 invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
return invoice return invoice
@@ -159,17 +172,21 @@ class POSInvoiceMergeLog(Document):
return sales_invoice return sales_invoice
def update_pos_invoices(self, sales_invoice, credit_note): def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for d in self.pos_invoices: for doc in invoice_docs:
doc = frappe.get_doc('POS Invoice', d.pos_invoice) doc.load_from_db()
if not doc.is_return: doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
doc.update({'consolidated_invoice': sales_invoice})
else:
doc.update({'consolidated_invoice': credit_note})
doc.set_status(update=True) doc.set_status(update=True)
doc.save() doc.save()
def get_all_invoices(): def cancel_linked_invoices(self):
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
if not si_name: continue
si = frappe.get_doc('Sales Invoice', si_name)
si.flags.ignore_validate = True
si.cancel()
def get_all_unconsolidated_invoices():
filters = { filters = {
'consolidated_invoice': [ 'in', [ '', None ]], 'consolidated_invoice': [ 'in', [ '', None ]],
'status': ['not in', ['Consolidated']], 'status': ['not in', ['Consolidated']],
@@ -180,7 +197,7 @@ def get_all_invoices():
return pos_invoices return pos_invoices
def get_invoices_customer_map(pos_invoices): def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] } # pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
pos_invoice_customer_map = {} pos_invoice_customer_map = {}
for invoice in pos_invoices: for invoice in pos_invoices:
@@ -190,20 +207,82 @@ def get_invoices_customer_map(pos_invoices):
return pos_invoice_customer_map return pos_invoice_customer_map
def merge_pos_invoices(pos_invoices=[]): def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
if not pos_invoices: invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
pos_invoices = get_all_invoices() invoice_by_customer = get_invoice_customer_map(invoices)
pos_invoice_map = get_invoices_customer_map(pos_invoices) if len(invoices) >= 5 and closing_entry:
create_merge_logs(pos_invoice_map) enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
closing_entry.set_status(update=True, status='Queued')
else:
create_merge_logs(invoice_by_customer, closing_entry)
def create_merge_logs(pos_invoice_customer_map): def unconsolidate_pos_invoices(closing_entry):
for customer, invoices in iteritems(pos_invoice_customer_map): merge_logs = frappe.get_all(
'POS Invoice Merge Log',
filters={ 'pos_closing_entry': closing_entry.name },
pluck='name'
)
if len(merge_logs) >= 5:
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
closing_entry.set_status(update=True, status='Queued')
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log') merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(nowdate()) merge_log.posting_date = getdate(nowdate())
merge_log.customer = customer merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices) merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True) merge_log.save(ignore_permissions=True)
merge_log.submit() merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
def cancel_merge_logs(merge_logs, closing_entry={}):
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
if closing_entry:
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, invoice_by_customer, closing_entry):
check_scheduler_status()
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
enqueue(
job,
queue="long",
timeout=10000,
event="processing_merge_logs",
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
now=frappe.conf.developer_mode or frappe.flags.in_test
)
if job == create_merge_logs:
msg = _('POS Invoices will be consolidated in a background process')
else:
msg = _('POS Invoices will be unconsolidated in a background process')
frappe.msgprint(msg, alert=1)
def check_scheduler_status():
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True

View File

@@ -7,7 +7,7 @@ import frappe
import unittest import unittest
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
class TestPOSInvoiceMergeLog(unittest.TestCase): class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -34,7 +34,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
}) })
pos_inv3.submit() pos_inv3.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
@@ -79,7 +79,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn.paid_amount = -300 pos_inv_cn.paid_amount = -300
pos_inv_cn.submit() pos_inv_cn.submit()
merge_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))

View File

@@ -6,7 +6,6 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, get_link_to_form from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater): class POSOpeningEntry(StatusUpdater):
@@ -25,10 +24,11 @@ class POSOpeningEntry(StatusUpdater):
def validate_payment_method_account(self): def validate_payment_method_account(self):
invalid_modes = [] invalid_modes = []
for d in self.balance_details: for d in self.balance_details:
account = frappe.db.get_value("Mode of Payment Account", if d.mode_of_payment:
{"parent": d.mode_of_payment, "company": self.company}, "default_account") account = frappe.db.get_value("Mode of Payment Account",
if not account: {"parent": d.mode_of_payment, "company": self.company}, "default_account")
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
if invalid_modes: if invalid_modes:
if invalid_modes == 1: if invalid_modes == 1:

View File

@@ -5,7 +5,7 @@
frappe.listview_settings['POS Opening Entry'] = { frappe.listview_settings['POS Opening Entry'] = {
get_indicator: function(doc) { get_indicator: function(doc) {
var status_color = { var status_color = {
"Draft": "grey", "Draft": "red",
"Open": "orange", "Open": "orange",
"Closed": "green", "Closed": "green",
"Cancelled": "red" "Cancelled": "red"

View File

@@ -57,6 +57,8 @@ frappe.ui.form.on('POS Profile', {
} }
}; };
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
@@ -67,6 +69,7 @@ frappe.ui.form.on('POS Profile', {
company: function(frm) { company: function(frm) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}, },
toggle_display_account_head: function(frm) { toggle_display_account_head: function(frm) {

View File

@@ -12,8 +12,6 @@
"company", "company",
"country", "country",
"column_break_9", "column_break_9",
"update_stock",
"ignore_pricing_rule",
"warehouse", "warehouse",
"campaign", "campaign",
"company_address", "company_address",
@@ -25,8 +23,14 @@
"hide_images", "hide_images",
"hide_unavailable_items", "hide_unavailable_items",
"auto_add_item_to_cart", "auto_add_item_to_cart",
"item_groups",
"column_break_16", "column_break_16",
"update_stock",
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
"section_break_23",
"item_groups",
"column_break_25",
"customer_groups", "customer_groups",
"section_break_16", "section_break_16",
"print_format", "print_format",
@@ -309,6 +313,7 @@
"default": "1", "default": "1",
"fieldname": "update_stock", "fieldname": "update_stock",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Update Stock", "label": "Update Stock",
"read_only": 1 "read_only": 1
}, },
@@ -329,13 +334,34 @@
"fieldname": "auto_add_item_to_cart", "fieldname": "auto_add_item_to_cart",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Automatically Add Filtered Item To Cart" "label": "Automatically Add Filtered Item To Cart"
},
{
"default": "0",
"fieldname": "allow_rate_change",
"fieldtype": "Check",
"label": "Allow User to Edit Rate"
},
{
"default": "0",
"fieldname": "allow_discount_change",
"fieldtype": "Check",
"label": "Allow User to Edit Discount"
},
{
"collapsible": 1,
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-12-20 13:59:28.877572", "modified": "2021-01-06 14:42:41.713864",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -56,6 +56,7 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(details.get("discount_percentage"), 10) self.assertEqual(details.get("discount_percentage"), 10)
prule = frappe.get_doc(test_record.copy()) prule = frappe.get_doc(test_record.copy())
prule.priority = 1
prule.applicable_for = "Customer" prule.applicable_for = "Customer"
prule.title = "_Test Pricing Rule for Customer" prule.title = "_Test Pricing Rule for Customer"
self.assertRaises(MandatoryError, prule.insert) self.assertRaises(MandatoryError, prule.insert)
@@ -261,6 +262,7 @@ class TestPricingRule(unittest.TestCase):
"rate_or_discount": "Discount Percentage", "rate_or_discount": "Discount Percentage",
"rate": 0, "rate": 0,
"discount_percentage": 17.5, "discount_percentage": 17.5,
"priority": 1,
"company": "_Test Company" "company": "_Test Company"
}).insert() }).insert()
@@ -557,6 +559,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0, "rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '', "condition": args.condition or '',
"priority": 1,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })

View File

@@ -41,10 +41,11 @@ def get_pricing_rules(args, doc=None):
if not pricing_rules: return [] if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules): if apply_multiple_pricing_rules(pricing_rules):
pricing_rules = sorted_by_priority(pricing_rules) pricing_rules = sorted_by_priority(pricing_rules, args, doc)
for pricing_rule in pricing_rules: for pricing_rule in pricing_rules:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc) if isinstance(pricing_rule, list):
if pricing_rule: rules.extend(pricing_rule)
else:
rules.append(pricing_rule) rules.append(pricing_rule)
else: else:
pricing_rule = filter_pricing_rules(args, pricing_rules, doc) pricing_rule = filter_pricing_rules(args, pricing_rules, doc)
@@ -53,17 +54,22 @@ def get_pricing_rules(args, doc=None):
return rules return rules
def sorted_by_priority(pricing_rules): def sorted_by_priority(pricing_rules, args, doc=None):
# If more than one pricing rules, then sort by priority # If more than one pricing rules, then sort by priority
pricing_rules_list = [] pricing_rules_list = []
pricing_rule_dict = {} pricing_rule_dict = {}
for pricing_rule in pricing_rules:
if not pricing_rule.get("priority"): continue
pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule) for pricing_rule in pricing_rules:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
if pricing_rule:
if not pricing_rule.get('priority'):
pricing_rule['priority'] = 1
if pricing_rule.get('apply_multiple_pricing_rules'):
pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule)
for key in sorted(pricing_rule_dict): for key in sorted(pricing_rule_dict):
pricing_rules_list.append(pricing_rule_dict.get(key)) pricing_rules_list.extend(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules return pricing_rules_list or pricing_rules
@@ -144,9 +150,7 @@ def apply_multiple_pricing_rules(pricing_rules):
if not apply_multiple_rule: return False if not apply_multiple_rule: return False
if (apply_multiple_rule return True
and len(apply_multiple_rule) == len(pricing_rules)):
return True
def _get_tree_conditions(args, parenttype, table, allow_blank=True): def _get_tree_conditions(args, parenttype, table, allow_blank=True):
field = frappe.scrub(parenttype) field = frappe.scrub(parenttype)
@@ -264,18 +268,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if max_priority: if max_priority:
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules)) pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
# apply internal priority
all_fields = ["item_code", "item_group", "brand", "customer", "customer_group", "territory",
"supplier", "supplier_group", "campaign", "sales_partner", "variant_of"]
if len(pricing_rules) > 1:
for field_set in [["item_code", "variant_of", "item_group", "brand"],
["customer", "customer_group", "territory"], ["supplier", "supplier_group"]]:
remaining_fields = list(set(all_fields) - set(field_set))
if if_all_rules_same(pricing_rules, remaining_fields):
pricing_rules = apply_internal_priority(pricing_rules, field_set, args)
break
if pricing_rules and not isinstance(pricing_rules, list): if pricing_rules and not isinstance(pricing_rules, list):
pricing_rules = list(pricing_rules) pricing_rules = list(pricing_rules)

View File

@@ -26,6 +26,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}; };
}); });
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
this._super(); this._super();
@@ -41,6 +46,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if (this.frm.doc.supplier && this.frm.doc.__islocal) { if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier'); this.frm.trigger('supplier');
} }
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh: function(doc) { refresh: function(doc) {
@@ -268,8 +275,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
supplier: function() { supplier: function() {
var me = this; var me = this;
if(this.frm.updating_party_details)
// Do not update if inter company reference is there as the details will already be updated
if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
return; return;
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{ {
posting_date: this.frm.doc.posting_date, posting_date: this.frm.doc.posting_date,
@@ -498,7 +508,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
frappe.ui.form.on("Purchase Invoice", { frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Purchase Invoice': 'Debit Note', 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment'
} }
@@ -511,15 +521,6 @@ frappe.ui.form.on("Purchase Invoice", {
} }
} }
} }
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
}, },
onload: function(frm) { onload: function(frm) {

View File

@@ -57,8 +57,9 @@
"set_warehouse", "set_warehouse",
"rejected_warehouse", "rejected_warehouse",
"col_break_warehouse", "col_break_warehouse",
"is_subcontracted", "set_from_warehouse",
"supplier_warehouse", "supplier_warehouse",
"is_subcontracted",
"items_section", "items_section",
"update_stock", "update_stock",
"scan_barcode", "scan_barcode",
@@ -515,6 +516,7 @@
}, },
{ {
"depends_on": "update_stock", "depends_on": "update_stock",
"description": "Sets 'Accepted Warehouse' in each row of the items table.",
"fieldname": "set_warehouse", "fieldname": "set_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Set Accepted Warehouse", "label": "Set Accepted Warehouse",
@@ -543,17 +545,6 @@
"options": "No\nYes", "options": "No\nYes",
"print_hide": 1 "print_hide": 1
}, },
{
"depends_on": "eval:doc.is_subcontracted==\"Yes\"",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{ {
"fieldname": "items_section", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -1232,7 +1223,9 @@
"fieldname": "inter_company_invoice_reference", "fieldname": "inter_company_invoice_reference",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Inter Company Invoice Reference", "label": "Inter Company Invoice Reference",
"no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -1356,13 +1349,36 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Represents Company", "label": "Represents Company",
"options": "Company" "options": "Company"
},
{
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
"description": "Sets 'From Warehouse' in each row of the items table.",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"label": "Set From Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"depends_on": "eval:doc.update_stock && doc.is_subcontracted==\"Yes\"",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"width": "50px"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-11 12:46:12.796378", "modified": "2021-03-09 21:56:28.748582",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -480,7 +480,7 @@ class PurchaseInvoice(BuyingController):
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
if grand_total and not self.is_internal_transfer(): if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle # Did not use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate, grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total")) self.precision("grand_total"))
gl_entries.append( gl_entries.append(
@@ -511,8 +511,8 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {} voucher_wise_stock_value = {}
if self.update_stock: if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry', for d in frappe.get_all('Stock Ledger Entry',
fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes") valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation') if d.category in ('Valuation', 'Total and Valuation')
@@ -563,16 +563,17 @@ class PurchaseInvoice(BuyingController):
) )
else: else:
gl_entries.append( if not self.is_internal_transfer():
self.get_gl_dict({ gl_entries.append(
"account": item.expense_account, self.get_gl_dict({
"against": self.supplier, "account": item.expense_account,
"debit": warehouse_debit_amount, "against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": warehouse_debit_amount,
"cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"project": item.project or self.project "cost_center": item.cost_center,
}, account_currency, item=item) "project": item.project or self.project
) }, account_currency, item=item)
)
# Amount added through landed-cost-voucher # Amount added through landed-cost-voucher
if landed_cost_entries: if landed_cost_entries:
@@ -582,7 +583,8 @@ class PurchaseInvoice(BuyingController):
"against": item.expense_account, "against": item.expense_account,
"cost_center": item.cost_center, "cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount), "credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project "project": item.project or self.project
}, item=item)) }, item=item))
@@ -624,13 +626,14 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr: if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account expense_account = service_received_but_not_billed_account
gl_entries.append(self.get_gl_dict({ if not self.is_internal_transfer():
"account": expense_account, gl_entries.append(self.get_gl_dict({
"against": self.supplier, "account": expense_account,
"debit": amount, "against": self.supplier,
"cost_center": item.cost_center, "debit": amount,
"project": item.project or self.project "cost_center": item.cost_center,
}, account_currency, item=item)) "project": item.project or self.project
}, account_currency, item=item))
# If asset is bought through this document and not linked to PR # If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount: if self.update_stock and item.landed_cost_voucher_amount:
@@ -795,10 +798,10 @@ class PurchaseInvoice(BuyingController):
# Stock ledger value is not matching with the warehouse amount # Stock ledger value is not matching with the warehouse amount
if (self.update_stock and voucher_wise_stock_value.get(item.name) and if (self.update_stock and voucher_wise_stock_value.get(item.name) and
warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)): warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)):
cost_of_goods_sold_account = self.get_company_default("default_expense_account") cost_of_goods_sold_account = self.get_company_default("default_expense_account")
stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision) stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
stock_adjustment_amt = warehouse_debit_amount - stock_amount stock_adjustment_amt = warehouse_debit_amount - stock_amount
gl_entries.append( gl_entries.append(

View File

@@ -426,26 +426,31 @@ class TestPurchaseInvoice(unittest.TestCase):
) )
def test_total_purchase_cost_for_project(self): def test_total_purchase_cost_for_project(self):
make_project({'project_name':'_Test Project'}) if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}):
project = make_project({'project_name':'_Test Project for Purchase'})
else:
project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"})
existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") from `tabPurchase Invoice Item`
where project = '{0}'
and docstatus=1""".format(project.name))
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project") pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000) existing_purchase_cost + 15000)
pi1 = make_purchase_invoice(qty=10, project="_Test Project") pi1 = make_purchase_invoice(qty=10, project=project.name)
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15500) existing_purchase_cost + 15500)
pi1.cancel() pi1.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000) existing_purchase_cost + 15000)
pi.cancel() pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice_with_perpetual_inventory(self): def test_return_purchase_invoice_with_perpetual_inventory(self):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
@@ -860,17 +865,17 @@ class TestPurchaseInvoice(unittest.TestCase):
}) })
pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
pi.items[0].project = item_project.project_name pi.items[0].project = item_project.name
pi.project = project.project_name pi.project = project.name
pi.submit() pi.submit()
expected_values = { expected_values = {
"Creditors - _TC": { "Creditors - _TC": {
"project": project.project_name "project": project.name
}, },
"_Test Account Cost for Goods Sold - _TC": { "_Test Account Cost for Goods Sold - _TC": {
"project": item_project.project_name "project": item_project.name
} }
} }

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@@ -39,6 +40,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_22", "section_break_22",
"net_rate", "net_rate",
@@ -87,6 +89,7 @@
"po_detail", "po_detail",
"purchase_receipt", "purchase_receipt",
"pr_detail", "pr_detail",
"sales_invoice_item",
"item_weight_details", "item_weight_details",
"weight_per_unit", "weight_per_unit",
"total_weight", "total_weight",
@@ -553,8 +556,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"label": "Brand", "label": "Brand",
"print_hide": 1, "options": "Brand",
"options": "Brand" "print_hide": 1
}, },
{ {
"fetch_from": "item_code.item_group", "fetch_from": "item_code.item_group",
@@ -562,9 +565,9 @@
"fieldname": "item_group", "fieldname": "item_group",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Item Group", "label": "Item Group",
"options": "Item Group",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"options": "Item Group"
}, },
{ {
"description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", "description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges",
@@ -759,10 +762,11 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
"fieldname": "from_warehouse", "fieldname": "from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Supplier Warehouse", "label": "From Warehouse",
"options": "Warehouse" "options": "Warehouse"
}, },
{ {
@@ -779,11 +783,28 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
},
{
"fieldname": "sales_invoice_item",
"fieldtype": "Data",
"label": "Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2020-08-20 11:48:01.398356", "links": [],
"modified": "2021-01-30 21:43:21.488258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -5,18 +5,22 @@
cur_frm.pformat.print_heading = 'Invoice'; cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({
setup: function(doc) { setup: function(doc) {
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
this._super(doc); this._super(doc);
}, },
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() { onload: function() {
var me = this; var me = this;
this._super(); this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0); this.frm.set_df_property("debit_to", "print_hide", 0);
@@ -33,6 +37,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
me.frm.refresh_fields(); me.frm.refresh_fields();
} }
erpnext.queries.setup_warehouse_query(this.frm); erpnext.queries.setup_warehouse_query(this.frm);
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}, },
refresh: function(doc, dt, dn) { refresh: function(doc, dt, dn) {
@@ -126,16 +131,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.set_default_print_format(); this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
frappe.model.with_doc("Customer", me.frm.doc.customer, function() { let internal = me.frm.doc.is_internal_customer;
var customer = frappe.model.get_doc("Customer", me.frm.doc.customer); if (internal) {
var internal = customer.is_internal_customer; let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" :
var disabled = customer.disabled; "Inter Company Purchase Invoice";
if (internal == 1 && disabled == 0) {
me.frm.add_custom_button("Inter Company Invoice", function() { me.frm.add_custom_button(button_label, function() {
me.make_inter_company_invoice(); me.make_inter_company_invoice();
}, __('Create')); }, __('Create'));
} }
});
} }
}, },
@@ -571,15 +575,6 @@ frappe.ui.form.on('Sales Invoice', {
}; };
}); });
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query("unrealized_profit_loss_account", function() { frm.set_query("unrealized_profit_loss_account", function() {
return { return {
filters: { filters: {
@@ -592,7 +587,7 @@ frappe.ui.form.on('Sales Invoice', {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery', 'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return', 'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request', 'Payment Request': 'Payment Request',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment'
}, },

View File

@@ -60,6 +60,8 @@
"ignore_pricing_rule", "ignore_pricing_rule",
"sec_warehouse", "sec_warehouse",
"set_warehouse", "set_warehouse",
"column_break_55",
"set_target_warehouse",
"items_section", "items_section",
"update_stock", "update_stock",
"scan_barcode", "scan_barcode",
@@ -1969,13 +1971,31 @@
"label": "Represents Company", "label": "Represents Company",
"options": "Company", "options": "Company",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_55",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.is_internal_customer && doc.update_stock",
"fieldname": "set_target_warehouse",
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [
"modified": "2020-12-11 12:48:31.769958", {
"custom": 1,
"group": "Reference",
"link_doctype": "POS Invoice",
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-01-12 12:16:15.192520",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -4,9 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
@@ -21,6 +21,8 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points
from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.deferred_revenue import validate_service_stop_date
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -180,6 +182,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
@@ -231,7 +236,25 @@ class SalesInvoice(SellingController):
if len(self.payments) == 0 and self.is_pos: if len(self.payments) == 0 and self.is_pos:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
def check_if_consolidated_invoice(self):
# since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice
if self.doctype == "Sales Invoice" and self.is_consolidated:
invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice"
pos_closing_entry = frappe.get_all(
"POS Invoice Merge Log",
filters={ invoice_or_credit_note: self.name },
pluck="pos_closing_entry"
)
if pos_closing_entry:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
)
frappe.throw(msg, title=_("Not Allowed"))
def before_cancel(self): def before_cancel(self):
self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel() super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
@@ -433,7 +456,9 @@ class SalesInvoice(SellingController):
if not for_validate and not self.customer: if not for_validate and not self.customer:
self.customer = pos.customer self.customer = pos.customer
self.ignore_pricing_rule = pos.ignore_pricing_rule if not for_validate:
self.ignore_pricing_rule = pos.ignore_pricing_rule
if pos.get('account_for_change_amount'): if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount') self.account_for_change_amount = pos.get('account_for_change_amount')
@@ -549,7 +574,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc) self.against_income_account = ','.join(against_acc)
def add_remarks(self): def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks' if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended
@@ -1529,7 +1559,7 @@ def validate_inter_company_transaction(doc, doctype):
details = get_inter_company_details(doc, doctype) details = get_inter_company_details(doc, doctype)
price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list
valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1})
if not valid_price_list: if not valid_price_list and not doc.is_internal_transfer():
frappe.throw(_("Selected Price List should have buying and selling fields checked.")) frappe.throw(_("Selected Price List should have buying and selling fields checked."))
party = details.get("party") party = details.get("party")
@@ -1552,6 +1582,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]: if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name) source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
source_document_warehouse_field = 'target_warehouse' source_document_warehouse_field = 'target_warehouse'
target_document_warehouse_field = 'from_warehouse' target_document_warehouse_field = 'from_warehouse'
else: else:
@@ -1565,6 +1596,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.run_method("set_missing_values") target.run_method("set_missing_values")
set_purchase_references(target)
def update_details(source_doc, target_doc, source_parent): def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name target_doc.inter_company_invoice_reference = source_doc.name
@@ -1572,19 +1604,38 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency')
target_doc.company = details.get("company") target_doc.company = details.get("company")
target_doc.supplier = details.get("party") target_doc.supplier = details.get("party")
target_doc.is_internal_supplier = 1
target_doc.ignore_pricing_rule = 1
target_doc.buying_price_list = source_doc.selling_price_list target_doc.buying_price_list = source_doc.selling_price_list
# Invert Addresses
update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address)
update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address)
if currency: if currency:
target_doc.currency = currency target_doc.currency = currency
update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company,
doctype=target_doc.doctype, party_address=target_doc.supplier_address,
company_address=target_doc.shipping_address)
else: else:
currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency')
target_doc.company = details.get("company") target_doc.company = details.get("company")
target_doc.customer = details.get("party") target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list target_doc.selling_price_list = source_doc.buying_price_list
update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address)
update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address)
update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address)
if currency: if currency:
target_doc.currency = currency target_doc.currency = currency
update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company,
doctype=target_doc.doctype, party_address=target_doc.customer_address,
company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name)
item_field_map = { item_field_map = {
"doctype": target_doctype + " Item", "doctype": target_doctype + " Item",
"field_no_map": [ "field_no_map": [
@@ -1592,25 +1643,33 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"expense_account", "expense_account",
"cost_center", "cost_center",
"warehouse" "warehouse"
] ],
"field_map": {
'rate': 'rate',
}
} }
if source_doc.get('update_stock'): if doctype in ["Sales Invoice", "Sales Order"]:
item_field_map.update({ item_field_map["field_map"].update({
'field_map': { "name": target_detail_field,
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
}
}) })
if source_doc.get('update_stock'):
item_field_map["field_map"].update({
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
})
doclist = get_mapped_doc(doctype, source_name, { doclist = get_mapped_doc(doctype, source_name, {
doctype: { doctype: {
"doctype": target_doctype, "doctype": target_doctype,
"postprocess": update_details, "postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": [ "field_no_map": [
"taxes_and_charges" "taxes_and_charges",
"set_warehouse",
"shipping_address"
] ]
}, },
doctype +" Item": item_field_map doctype +" Item": item_field_map
@@ -1619,6 +1678,110 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
return doclist return doclist
def set_purchase_references(doc):
# add internal PO or PR links if any
if doc.is_internal_transfer():
if doc.doctype == 'Purchase Receipt':
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
if so_item_map:
pd_item_map, parent_child_map, warehouse_map = \
get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map)
elif doc.doctype == 'Purchase Invoice':
dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference)
# First check for Purchase receipt
if list(dn_item_map.values()):
pd_item_map, parent_child_map, warehouse_map = \
get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item')
update_pi_items(doc, 'pr_detail', 'purchase_receipt',
dn_item_map, pd_item_map, parent_child_map, warehouse_map)
if list(so_item_map.values()):
pd_item_map, parent_child_map, warehouse_map = \
get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
update_pi_items(doc, 'po_detail', 'purchase_order',
so_item_map, pd_item_map, parent_child_map, warehouse_map)
def update_pi_items(doc, detail_field, parent_field, sales_item_map,
purchase_item_map, parent_child_map, warehouse_map):
for item in doc.get('items'):
item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item)))
item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
if doc.update_stock:
item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
for item in doc.get('items'):
item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item))
item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item))
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
def get_delivery_note_details(internal_reference):
so_item_map = {}
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
filters={'parent': internal_reference})
for d in si_item_details:
so_item_map.setdefault(d.name, d.so_detail)
return so_item_map
def get_sales_invoice_details(internal_reference):
dn_item_map = {}
so_item_map = {}
si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail',
'dn_detail'], filters={'parent': internal_reference})
for d in si_item_details:
if d.dn_detail:
dn_item_map.setdefault(d.name, d.dn_detail)
if d.so_detail:
so_item_map.setdefault(d.name, d.so_detail)
return dn_item_map, so_item_map
def get_pd_details(doctype, sd_detail_map, sd_detail_field):
pd_item_map = {}
accepted_warehouse_map = {}
parent_child_map = {}
pd_item_details = frappe.get_all(doctype,
fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))})
for d in pd_item_details:
pd_item_map.setdefault(d.get(sd_detail_field), d.name)
parent_child_map.setdefault(d.get(sd_detail_field), d.parent)
accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse)
return pd_item_map, parent_child_map, accepted_warehouse_map
def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None,
company_address=None, shipping_address_name=None, master_doctype=None):
# Update Party Details
party_details = get_party_details(party=party, party_type=party_type, company=company,
doctype=doctype, party_address=party_address, company_address=company_address,
shipping_address=shipping_address_name)
# Update taxes and charges if any
doc.taxes_and_charges = party_details.get('taxes_and_charges')
doc.set('taxes', party_details.get('taxes'))
def update_address(doc, address_field, address_display_field, address_name):
doc.set(address_field, address_name)
fetch_values = get_fetch_values(doc.doctype, address_field, address_name)
for key, value in fetch_values.items():
doc.set(key, value)
doc.set(address_display_field, get_address_display(doc.get(address_field)))
@frappe.whitelist() @frappe.whitelist()
def get_loyalty_programs(customer): def get_loyalty_programs(customer):
''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' ''' sets applicable loyalty program to the customer or returns a list of applicable programs '''
@@ -1694,6 +1857,7 @@ def get_mode_of_payment_info(mode_of_payment, company):
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1) (company, mode_of_payment), as_dict=1)
@frappe.whitelist()
def create_dunning(source_name, target_doc=None): def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount

View File

@@ -22,6 +22,7 @@ from erpnext.regional.india.utils import get_ewb_data
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
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.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.utils import get_incoming_rate
class TestSalesInvoice(unittest.TestCase): class TestSalesInvoice(unittest.TestCase):
def make(self): def make(self):
@@ -1573,17 +1574,17 @@ class TestSalesInvoice(unittest.TestCase):
}) })
sales_invoice = create_sales_invoice(do_not_save=1) sales_invoice = create_sales_invoice(do_not_save=1)
sales_invoice.items[0].project = item_project.project_name sales_invoice.items[0].project = item_project.name
sales_invoice.project = project.project_name sales_invoice.project = project.name
sales_invoice.submit() sales_invoice.submit()
expected_values = { expected_values = {
"Debtors - _TC": { "Debtors - _TC": {
"project": project.project_name "project": project.name
}, },
"Sales - _TC": { "Sales - _TC": {
"project": item_project.project_name "project": item_project.name
} }
} }
@@ -1770,59 +1771,82 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier") self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
# def test_internal_transfer_gl_entry(self): def test_internal_transfer_gl_entry(self):
# ## Create internal transfer account ## Create internal transfer account
# account = create_account(account_name="Unrealized Profit", account = create_account(account_name="Unrealized Profit",
# parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
# frappe.db.set_value('Company', '_Test Company with perpetual inventory', frappe.db.set_value('Company', '_Test Company with perpetual inventory',
# 'unrealized_profit_loss_account', account) 'unrealized_profit_loss_account', account)
# customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
# "_Test Company with perpetual inventory") "_Test Company with perpetual inventory")
# create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
# "_Test Company with perpetual inventory") "_Test Company with perpetual inventory")
# si = create_sales_invoice( si = create_sales_invoice(
# company = "_Test Company with perpetual inventory", company = "_Test Company with perpetual inventory",
# customer = customer, customer = customer,
# debit_to = "Debtors - TCP1", debit_to = "Debtors - TCP1",
# warehouse = "Stores - TCP1", warehouse = "Stores - TCP1",
# income_account = "Sales - TCP1", income_account = "Sales - TCP1",
# expense_account = "Cost of Goods Sold - TCP1", expense_account = "Cost of Goods Sold - TCP1",
# cost_center = "Main - TCP1", cost_center = "Main - TCP1",
# currency = "INR", currency = "INR",
# do_not_save = 1 do_not_save = 1
# ) )
# si.selling_price_list = "_Test Price List Rest of the World" si.selling_price_list = "_Test Price List Rest of the World"
# si.update_stock = 1 si.update_stock = 1
# si.items[0].target_warehouse = 'Work In Progress - TCP1' si.items[0].target_warehouse = 'Work In Progress - TCP1'
# add_taxes(si) add_taxes(si)
# si.save() si.save()
# si.submit()
# target_doc = make_inter_company_transaction("Sales Invoice", si.name) rate = 0.0
# target_doc.company = '_Test Company with perpetual inventory' for d in si.get('items'):
# target_doc.items[0].warehouse = 'Finished Goods - TCP1' rate = get_incoming_rate({
# add_taxes(target_doc) "item_code": d.item_code,
# target_doc.save() "warehouse": d.warehouse,
# target_doc.submit() "posting_date": si.posting_date,
"posting_time": si.posting_time,
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.serial_no,
"company": si.company,
"voucher_type": 'Sales Invoice',
"voucher_no": si.name,
"allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False)
# si_gl_entries = [ rate = flt(rate, 2)
# ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
# ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
# ]
# check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) si.submit()
# pi_gl_entries = [ target_doc = make_inter_company_transaction("Sales Invoice", si.name)
# ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], target_doc.company = '_Test Company with perpetual inventory'
# ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] target_doc.items[0].warehouse = 'Finished Goods - TCP1'
# ] add_taxes(target_doc)
target_doc.save()
target_doc.submit()
# check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) tax_amount = flt(rate * (12/100), 2)
si_gl_entries = [
["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()],
["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()]
]
check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
pi_gl_entries = [
["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()],
["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()]
]
# Sale and Purchase both should be at valuation rate
self.assertEqual(si.items[0].rate, rate)
self.assertEqual(target_doc.items[0].rate, rate)
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self): def test_eway_bill_json(self):
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
@@ -1861,23 +1885,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_json(self): def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice from erpnext.regional.india.e_invoice.utils import make_einvoice
customer_gstin = '27AACCM7806M1Z3'
customer_gstin_dtls = {
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
company_gstin = '27AAECE4835E1ZR'
company_gstin_dtls = {
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
# set cache gstin details to avoid fetching details which will require connection to GSP servers
frappe.local.gstin_cache = {}
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'
si.items = [] si.items = []
@@ -1885,8 +1892,8 @@ class TestSalesInvoice(unittest.TestCase):
"item_code": "_Test Item", "item_code": "_Test Item",
"uom": "Nos", "uom": "Nos",
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"qty": 2, "qty": 2000,
"rate": 100, "rate": 12,
"income_account": "Sales - _TC", "income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC", "expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",
@@ -1895,31 +1902,52 @@ class TestSalesInvoice(unittest.TestCase):
"item_code": "_Test Item 2", "item_code": "_Test Item 2",
"uom": "Nos", "uom": "Nos",
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"qty": 4, "qty": 420,
"rate": 150, "rate": 15,
"income_account": "Sales - _TC", "income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC", "expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",
}) })
si.discount_amount = 100
si.save() si.save()
einvoice = make_einvoice(si) einvoice = make_einvoice(si)
total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) total_item_ass_value = 0
total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) total_item_cgst_value = 0
total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) total_item_sgst_value = 0
total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) total_item_igst_value = 0
total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) total_item_value = 0
for item in einvoice['ItemList']:
total_item_ass_value += item['AssAmt']
total_item_cgst_value += item['CgstAmt']
total_item_sgst_value += item['SgstAmt']
total_item_igst_value += item['IgstAmt']
total_item_value += item['TotItemVal']
self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
value_details = einvoice['ValDtls']
self.assertEqual(einvoice['Version'], '1.1') self.assertEqual(einvoice['Version'], '1.1')
self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) self.assertEqual(value_details['AssVal'], total_item_ass_value)
self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) self.assertEqual(value_details['IgstVal'], total_item_igst_value)
self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value)
calculated_invoice_value = \
value_details['AssVal'] + value_details['CgstVal'] \
+ value_details['SgstVal'] + value_details['IgstVal'] \
+ value_details['OthChrg'] - value_details['Discount']
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls']) self.assertTrue(einvoice['EwbDtls'])
def make_sales_invoice_for_ewaybill(): def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({ address = frappe.get_doc({
"address_line1": "_Test Address Line 1", "address_line1": "_Test Address Line 1",
@@ -1968,6 +1996,7 @@ def make_sales_invoice_for_ewaybill():
address.save() address.save()
def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'): if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({ frappe.get_doc({
"doctype": "Supplier", "doctype": "Supplier",
@@ -1978,12 +2007,17 @@ def make_sales_invoice_for_ewaybill():
"is_transporter": 1 "is_transporter": 1
}).insert() }).insert()
def make_sales_invoice_for_ewaybill():
make_test_address_for_ewaybill()
make_test_transporter_for_ewaybill()
gst_settings = frappe.get_doc("GST Settings") gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all( gst_account = frappe.get_all(
"GST Account", "GST Account",
fields=["cgst_account", "sgst_account", "igst_account"], fields=["cgst_account", "sgst_account", "igst_account"],
filters = {"company": "_Test Company"}) filters = {"company": "_Test Company"}
)
if not gst_account: if not gst_account:
gst_settings.append("gst_accounts", { gst_settings.append("gst_accounts", {
@@ -1995,7 +2029,7 @@ def make_sales_invoice_for_ewaybill():
gst_settings.save() gst_settings.save()
si = create_sales_invoice(do_not_save =1, rate = '60000') si = create_sales_invoice(do_not_save=1, rate='60000')
si.distance = 2000 si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing" si.company_address = "_Test Address for Eway bill-Billing"

View File

@@ -45,6 +45,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
@@ -565,11 +566,12 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
"fieldname": "target_warehouse", "fieldname": "target_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Customer Warehouse (Optional)", "label": "Target Warehouse",
"no_copy": 1, "no_copy": 1,
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1 "print_hide": 1
@@ -810,12 +812,20 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-23 19:59:04.879322", "modified": "2021-01-30 21:42:37.796771",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc):
validate_disabled(doc) validate_disabled(doc)
# Validate with existing taxes and charges template for unique tax category
validate_for_tax_category(doc)
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc) validate_inclusive_tax(tax, doc)
@@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc):
def validate_disabled(doc): def validate_disabled(doc):
if doc.is_default and doc.disabled: if doc.is_default and doc.disabled:
frappe.throw(_("Disabled template must not be default template")) frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc):
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))

View File

@@ -1,16 +1,18 @@
// 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
frappe.ui.form.on('Shipping Rule', { frappe.provide('erpnext.accounts.dimensions');
refresh: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frappe.ui.form.on('Shipping Rule', {
onload: function(frm) {
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
refresh: function(frm) {
frm.set_query("account", function() { frm.set_query("account", function() {
return { return {
filters: { filters: {

View File

@@ -446,7 +446,7 @@ class Subscription(Document):
if not self.generate_invoice_at_period_start: if not self.generate_invoice_at_period_start:
return False return False
if self.is_new_subscription(): if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True return True
# Check invoice dates and make sure it doesn't have outstanding invoices # Check invoice dates and make sure it doesn't have outstanding invoices

View File

@@ -44,9 +44,9 @@ def validate_accounting_period(gl_map):
frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}")
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True): def process_gl_map(gl_map, merge_entries=True, precision=None):
if merge_entries: if merge_entries:
gl_map = merge_similar_entries(gl_map) gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map: for entry in gl_map:
# toggle debit, credit if negative entry # toggle debit, credit if negative entry
if flt(entry.debit) < 0: if flt(entry.debit) < 0:
@@ -69,7 +69,7 @@ def process_gl_map(gl_map, merge_entries=True):
return gl_map return gl_map
def merge_similar_entries(gl_map): def merge_similar_entries(gl_map, precision=None):
merged_gl_map = [] merged_gl_map = []
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
for entry in gl_map: for entry in gl_map:
@@ -88,7 +88,9 @@ def merge_similar_entries(gl_map):
company = gl_map[0].company if gl_map else erpnext.get_default_company() company = gl_map[0].company if gl_map else erpnext.get_default_company()
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
# filter zero debit and credit entries # filter zero debit and credit entries
merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map)
@@ -132,8 +134,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.update(args) gle.update(args)
gle.flags.ignore_permissions = 1 gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost gle.flags.from_repost = from_repost
gle.insert() gle.flags.adv_adj = adv_adj
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) gle.flags.update_outstanding = update_outstanding or 'Yes'
gle.submit() gle.submit()
if not from_repost: if not from_repost:

View File

@@ -39,7 +39,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype))
party = party_details[party_type.lower()] party = party_details[party_type.lower()]
if not ignore_permissions and not frappe.has_permission(party_type, "read", party): if not ignore_permissions and not (frappe.has_permission(party_type, "read", party) or frappe.has_permission(party_type, "select", party)):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party) party = frappe.get_doc(party_type, party)

View File

@@ -152,7 +152,7 @@
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td> <td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr> </tr>

View File

@@ -47,21 +47,22 @@ def get_data(filters):
for d in gl_entries: for d in gl_entries:
asset_data = assets_details.get(d.against_voucher) asset_data = assets_details.get(d.against_voucher)
if not asset_data.get("accumulated_depreciation_amount"): if asset_data:
asset_data.accumulated_depreciation_amount = d.debit if not asset_data.get("accumulated_depreciation_amount"):
else: asset_data.accumulated_depreciation_amount = d.debit
asset_data.accumulated_depreciation_amount += d.debit else:
asset_data.accumulated_depreciation_amount += d.debit
row = frappe._dict(asset_data) row = frappe._dict(asset_data)
row.update({ row.update({
"depreciation_amount": d.debit, "depreciation_amount": d.debit,
"depreciation_date": d.posting_date, "depreciation_date": d.posting_date,
"amount_after_depreciation": (flt(row.gross_purchase_amount) - "amount_after_depreciation": (flt(row.gross_purchase_amount) -
flt(row.accumulated_depreciation_amount)), flt(row.accumulated_depreciation_amount)),
"depreciation_entry": d.voucher_no "depreciation_entry": d.voucher_no
}) })
data.append(row) data.append(row)
return data return data

View File

@@ -222,7 +222,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
set_gl_entries_by_account(start_date, set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters, end_date, root.lft, root.rgt, filters,
gl_entries_by_account, accounts_by_name, ignore_closing_entries=False) gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters) calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies) accumulate_values_into_parents(accounts, accounts_by_name, companies)
@@ -240,8 +240,7 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters): def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values(): for entries in gl_entries_by_account.values():
for entry in entries: for entry in entries:
key = entry.account_number or entry.account_name d = accounts_by_name.get(entry.account_name)
d = accounts_by_name.get(key)
if d: if d:
for company in companies: for company in companies:
# check if posting date is within the period # check if posting date is within the period
@@ -256,7 +255,8 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts""" """accumulate children's values in parent accounts"""
for d in reversed(accounts): for d in reversed(accounts):
if d.parent_account: if d.parent_account:
account = d.parent_account.split(' - ')[0].strip() account = d.parent_account_name
if not accounts_by_name.get(account): if not accounts_by_name.get(account):
continue continue
@@ -267,16 +267,34 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account]["opening_balance"] = \ accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, filters)
if not accounts: if not accounts:
return None, None return None, None
accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name return accounts, accounts_by_name
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
parent_name is `name` of parent account which could have other prefix
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
for account in accounts:
if account.parent_account:
account["parent_account_name"] = name_to_account_map[account.parent_account]
return accounts
def get_companies(filters): def get_companies(filters):
companies = {} companies = {}
all_companies = get_subsidiary_companies(filters.get('company')) all_companies = get_subsidiary_companies(filters.get('company'))
@@ -339,7 +357,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
return data return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account,
accounts_by_name, ignore_closing_entries=False): accounts_by_name, accounts, ignore_closing_entries=False):
"""Returns a dict like { "account": [gl entries], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company', company_lft, company_rgt = frappe.get_cached_value('Company',
@@ -381,16 +399,32 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name account_name = entry.account_name
validate_entries(key, entry, accounts_by_name) validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry) gl_entries_by_account.setdefault(account_name, []).append(entry)
return gl_entries_by_account return gl_entries_by_account
def validate_entries(key, entry, accounts_by_name): def get_account_details(account):
return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company',
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
def validate_entries(key, entry, accounts_by_name, accounts):
if key not in accounts_by_name: if key not in accounts_by_name:
field = "Account number" if entry.account_number else "Account name" args = get_account_details(entry.account)
frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
if args.parent_account:
parent_args = get_account_details(args.parent_account)
args.update({
'lft': parent_args.lft + 1,
'rgt': parent_args.rgt - 1,
'root_type': parent_args.root_type,
'report_type': parent_args.report_type
})
accounts_by_name.setdefault(key, args)
accounts.append(args)
def get_additional_conditions(from_date, ignore_closing_entries, filters): def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = [] additional_conditions = []
@@ -436,8 +470,7 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {} parent_children_map = {}
accounts_by_name = {} accounts_by_name = {}
for d in accounts: for d in accounts:
key = d.account_number or d.account_name accounts_by_name[d.account_name] = d
accounts_by_name[key] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d) parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = [] filtered_accounts = []

View File

@@ -49,12 +49,13 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
elif d.po_detail: elif d.po_detail:
purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, [])) purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, []))
expense_account = d.expense_account or aii_account_map.get(d.company) expense_account = d.unrealized_profit_loss_account or d.expense_account \
or aii_account_map.get(d.company)
row = { row = {
'item_code': d.item_code, 'item_code': d.item_code,
'item_name': item_record.item_name, 'item_name': item_record.item_name if item_record else d.item_name,
'item_group': item_record.item_group, 'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description, 'description': d.description,
'invoice': d.parent, 'invoice': d.parent,
'posting_date': d.posting_date, 'posting_date': d.posting_date,
@@ -315,7 +316,9 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
`tabPurchase Invoice`.unrealized_profit_loss_account,
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,

View File

@@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
'company': d.company, 'company': d.company,
'sales_order': d.sales_order, 'sales_order': d.sales_order,
'delivery_note': d.delivery_note, 'delivery_note': d.delivery_note,
'income_account': d.income_account, 'income_account': d.unrealized_profit_loss_account or d.income_account,
'cost_center': d.cost_center, 'cost_center': d.cost_center,
'stock_qty': d.stock_qty, 'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom 'stock_uom': d.stock_uom
@@ -379,6 +379,7 @@ def get_items(filters, additional_query_columns):
select select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,

View File

@@ -59,23 +59,111 @@ def validate_filters(filters):
def get_columns(filters): def get_columns(filters):
return [ return [
_("Payment Document") + ":: 100", {
_("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", "fieldname": "payment_document",
_("Party Type") + "::100", "label": _("Payment Document Type"),
_("Party") + ":Dynamic Link/Party Type:140", "fieldtype": "Data",
_("Posting Date") + ":Date:100", "width": 100
_("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"), },
_("Invoice Posting Date") + ":Date:130", {
_("Payment Due Date") + ":Date:130", "fieldname": "payment_entry",
_("Debit") + ":Currency:120", "label": _("Payment Document"),
_("Credit") + ":Currency:120", "fieldtype": "Dynamic Link",
_("Remarks") + "::150", "options": "payment_document",
_("Age") +":Int:40", "width": 160
"0-30:Currency:100", },
"30-60:Currency:100", {
"60-90:Currency:100", "fieldname": "party_type",
_("90-Above") + ":Currency:100", "label": _("Party Type"),
_("Delay in payment (Days)") + "::150" "fieldtype": "Data",
"width": 100
},
{
"fieldname": "party",
"label": _("Party"),
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 160
},
{
"fieldname": "posting_date",
"label": _("Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "invoice",
"label": _("Invoice"),
"fieldtype": "Link",
"options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice",
"width": 160
},
{
"fieldname": "invoice_posting_date",
"label": _("Invoice Posting Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "due_date",
"label": _("Payment Due Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "debit",
"label": _("Debit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "credit",
"label": _("Credit"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "remarks",
"label": _("Remarks"),
"fieldtype": "Data",
"width": 200
},
{
"fieldname": "age",
"label": _("Age"),
"fieldtype": "Int",
"width": 50
},
{
"fieldname": "range1",
"label": "0-30",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range2",
"label": "30-60",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range3",
"label": "60-90",
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "range4",
"label": _("90 Above"),
"fieldtype": "Currency",
"width": 140
},
{
"fieldname": "delay_in_payment",
"label": _("Delay in payment (Days)"),
"fieldtype": "Int",
"width": 100
}
] ]
def get_conditions(filters): def get_conditions(filters):

View File

@@ -14,13 +14,15 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if not filters: filters = {} if not filters: filters = {}
invoice_list = get_invoices(filters, additional_query_columns) invoice_list = get_invoices(filters, additional_query_columns)
columns, expense_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \
= get_columns(invoice_list, additional_table_columns)
if not invoice_list: if not invoice_list:
msgprint(_("No record found")) msgprint(_("No record found"))
return columns, invoice_list return columns, invoice_list
invoice_expense_map = get_invoice_expense_map(invoice_list) invoice_expense_map = get_invoice_expense_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_expense_map, expense_accounts) invoice_expense_map, expense_accounts)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
@@ -52,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
# map expense values # map expense values
base_net_total = 0 base_net_total = 0
for expense_acc in expense_accounts: for expense_acc in expense_accounts:
expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) if inv.is_internal_supplier and inv.company == inv.represents_company:
expense_amount = 0
else:
expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
base_net_total += expense_amount base_net_total += expense_amount
row.append(expense_amount) row.append(expense_amount)
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
row.append(flt(internal_invoice_map.get((inv.name, account))))
# net total # net total
row.append(base_net_total or inv.base_net_total) row.append(base_net_total or inv.base_net_total)
@@ -96,7 +105,8 @@ def get_columns(invoice_list, additional_table_columns):
"width": 80 "width": 80
} }
] ]
expense_accounts = tax_accounts = expense_columns = tax_columns = [] expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \
unrealized_profit_loss_account_columns = []
if invoice_list: if invoice_list:
expense_accounts = frappe.db.sql_list("""select distinct expense_account expense_accounts = frappe.db.sql_list("""select distinct expense_account
@@ -112,17 +122,25 @@ def get_columns(invoice_list, additional_table_columns):
and parent in (%s) order by account_head""" % and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
for account in tax_accounts: for account in tax_accounts:
if account not in expense_accounts: if account not in expense_accounts:
tax_columns.append(account + ":Currency/currency:120") tax_columns.append(account + ":Currency/currency:120")
columns = columns + expense_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ columns = columns + expense_columns + unrealized_profit_loss_account_columns + \
[_("Net Total") + ":Currency/currency:120"] + tax_columns + \
[_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120",
_("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"]
return columns, expense_accounts, tax_accounts return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""
@@ -199,6 +217,19 @@ def get_invoice_expense_map(invoice_list):
return invoice_expense_map return invoice_expense_map
def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
and is_internal_supplier = 1 and company = represents_company""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
if d.unrealized_profit_loss_account:
internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount)
return internal_invoice_map
def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
tax_details = frappe.db.sql(""" tax_details = frappe.db.sql("""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)

View File

@@ -15,13 +15,14 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
if not filters: filters = frappe._dict({}) if not filters: filters = frappe._dict({})
invoice_list = get_invoices(filters, additional_query_columns) invoice_list = get_invoices(filters, additional_query_columns)
columns, income_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns)
if not invoice_list: if not invoice_list:
msgprint(_("No record found")) msgprint(_("No record found"))
return columns, invoice_list return columns, invoice_list
invoice_income_map = get_invoice_income_map(invoice_list) invoice_income_map = get_invoice_income_map(invoice_list)
internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_income_map, income_accounts) invoice_income_map, income_accounts)
#Cost Center & Warehouse Map #Cost Center & Warehouse Map
@@ -70,12 +71,22 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
# map income values # map income values
base_net_total = 0 base_net_total = 0
for income_acc in income_accounts: for income_acc in income_accounts:
income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) if inv.is_internal_customer and inv.company == inv.represents_company:
income_amount = 0
else:
income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc))
base_net_total += income_amount base_net_total += income_amount
row.update({ row.update({
frappe.scrub(income_acc): income_amount frappe.scrub(income_acc): income_amount
}) })
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
row.update({
frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account)))
})
# net total # net total
row.update({'net_total': base_net_total or inv.base_net_total}) row.update({'net_total': base_net_total or inv.base_net_total})
@@ -230,6 +241,8 @@ def get_columns(invoice_list, additional_table_columns):
tax_accounts = [] tax_accounts = []
income_columns = [] income_columns = []
tax_columns = [] tax_columns = []
unrealized_profit_loss_accounts = []
unrealized_profit_loss_account_columns = []
if invoice_list: if invoice_list:
income_accounts = frappe.db.sql_list("""select distinct income_account income_accounts = frappe.db.sql_list("""select distinct income_account
@@ -243,12 +256,18 @@ def get_columns(invoice_list, additional_table_columns):
and parent in (%s) order by account_head""" % and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
for account in income_accounts: for account in income_accounts:
income_columns.append({ income_columns.append({
"label": account, "label": account,
"fieldname": frappe.scrub(account), "fieldname": frappe.scrub(account),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": 'currency', "options": "currency",
"width": 120 "width": 120
}) })
@@ -258,15 +277,24 @@ def get_columns(invoice_list, additional_table_columns):
"label": account, "label": account,
"fieldname": frappe.scrub(account), "fieldname": frappe.scrub(account),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": 'currency', "options": "currency",
"width": 120 "width": 120
}) })
for account in unrealized_profit_loss_accounts:
unrealized_profit_loss_account_columns.append({
"label": account,
"fieldname": frappe.scrub(account),
"fieldtype": "Currency",
"options": "currency",
"width": 120
})
net_total_column = [{ net_total_column = [{
"label": _("Net Total"), "label": _("Net Total"),
"fieldname": "net_total", "fieldname": "net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"options": 'currency', "options": "currency",
"width": 120 "width": 120
}] }]
@@ -301,9 +329,10 @@ def get_columns(invoice_list, additional_table_columns):
} }
] ]
columns = columns + income_columns + net_total_column + tax_columns + total_columns columns = columns + income_columns + unrealized_profit_loss_account_columns + \
net_total_column + tax_columns + total_columns
return columns, income_accounts, tax_accounts return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""
@@ -368,7 +397,8 @@ def get_invoices(filters, additional_query_columns):
return frappe.db.sql(""" return frappe.db.sql("""
select name, posting_date, debit_to, project, customer, select name, posting_date, debit_to, project, customer,
customer_name, owner, remarks, territory, tax_id, customer_group, customer_name, owner, remarks, territory, tax_id, customer_group,
base_net_total, base_grand_total, base_rounded_total, outstanding_amount {0} base_net_total, base_grand_total, base_rounded_total, outstanding_amount,
is_internal_customer, represents_company, company {0}
from `tabSales Invoice` from `tabSales Invoice`
where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') % where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') %
conditions, filters, as_dict=1) conditions, filters, as_dict=1)
@@ -385,6 +415,19 @@ def get_invoice_income_map(invoice_list):
return invoice_income_map return invoice_income_map
def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabSales Invoice` where name in (%s)
and is_internal_customer = 1 and company = represents_company""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
if d.unrealized_profit_loss_account:
internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount)
return internal_invoice_map
def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
tax_details = frappe.db.sql("""select parent, account_head, tax_details = frappe.db.sql("""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount sum(base_tax_amount_after_discount_amount) as tax_amount

View File

@@ -888,6 +888,11 @@ def get_coa(doctype, parent, is_root, chart=None):
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None): warehouse_account=None, company=None):
stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company)
repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account)
def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no): def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry` frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
@@ -895,21 +900,21 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items) precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
for voucher_type, voucher_no in future_stock_vouchers: gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), []) existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no) voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account) expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle: if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None): def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = [] future_stock_vouchers = []
values = [] values = []
@@ -922,6 +927,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses))) condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses values += for_warehouses
if company:
condition += " and company = %s"
values.append(company)
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle from `tabStock Ledger Entry` sle
where where
@@ -945,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
return gl_entries return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle): def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
matched = True matched = True
for entry in expected_gle: for entry in expected_gle:
account_existed = False account_existed = False
for e in existing_gle: for e in existing_gle:
if entry.account == e.account: if entry.account == e.account:
account_existed = True account_existed = True
if entry.account == e.account and entry.against_account == e.against_account \ if (entry.account == e.account and entry.against_account == e.against_account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and (entry.debit != e.debit or entry.credit != e.credit): and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False matched = False
break break
if not account_existed: if not account_existed:

View File

@@ -48,7 +48,7 @@ class CropCycle(Document):
def import_disease_tasks(self, disease, start_date): def import_disease_tasks(self, disease, start_date):
disease_doc = frappe.get_doc('Disease', disease) disease_doc = frappe.get_doc('Disease', disease)
self.create_task(disease_doc.treatment_task, self.name, start_date) self.create_task(disease_doc.treatment_task, self.project, start_date)
def create_project(self, period, crop_tasks): def create_project(self, period, crop_tasks):
project = frappe.get_doc({ project = frappe.get_doc({

View File

@@ -71,4 +71,4 @@ def check_task_creation():
def check_project_creation(): def check_project_creation():
return True if frappe.db.exists('Project', 'Basil from seed 2017') else False return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False

View File

@@ -2,6 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.provide("erpnext.asset"); frappe.provide("erpnext.asset");
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset', { frappe.ui.form.on('Asset', {
onload: function(frm) { onload: function(frm) {
@@ -32,13 +33,11 @@ frappe.ui.form.on('Asset', {
}; };
}); });
frm.set_query("cost_center", function() { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
return { },
"filters": {
"company": frm.doc.company, company: function(frm) {
} erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
};
});
}, },
setup: function(frm) { setup: function(frm) {

View File

@@ -1,6 +1,8 @@
// 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
frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset Value Adjustment', { frappe.ui.form.on('Asset Value Adjustment', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('company', 'cost_center', 'cost_center'); frm.add_fetch('company', 'cost_center', 'cost_center');
@@ -13,11 +15,19 @@ frappe.ui.form.on('Asset Value Adjustment', {
} }
}); });
}, },
onload: function(frm) { onload: function(frm) {
if(frm.is_new() && frm.doc.asset) { if(frm.is_new() && frm.doc.asset) {
frm.trigger("set_current_asset_value"); frm.trigger("set_current_asset_value");
} }
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
asset: function(frm) { asset: function(frm) {
frm.trigger("set_current_asset_value"); frm.trigger("set_current_asset_value");
}, },

View File

@@ -13,17 +13,14 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
class AssetValueAdjustment(Document): class AssetValueAdjustment(Document):
def validate(self): def validate(self):
self.validate_date() self.validate_date()
self.set_difference_amount()
self.set_current_asset_value() self.set_current_asset_value()
self.set_difference_amount()
def on_submit(self): def on_submit(self):
self.make_depreciation_entry() self.make_depreciation_entry()
self.reschedule_depreciations(self.new_asset_value) self.reschedule_depreciations(self.new_asset_value)
def on_cancel(self): def on_cancel(self):
if self.journal_entry:
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
self.reschedule_depreciations(self.current_asset_value) self.reschedule_depreciations(self.current_asset_value)
def validate_date(self): def validate_date(self):

View File

@@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying"); frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %}; {% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on("Purchase Order", { frappe.ui.form.on("Purchase Order", {
@@ -30,6 +30,10 @@ frappe.ui.form.on("Purchase Order", {
}, },
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm) { onload: function(frm) {
set_schedule_date(frm); set_schedule_date(frm);
if (!frm.doc.transaction_date){ if (!frm.doc.transaction_date){
@@ -39,6 +43,8 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() { erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc); return erpnext.queries.warehouse(frm.doc);
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
} }
}); });
@@ -58,8 +64,8 @@ frappe.ui.form.on("Purchase Order Item", {
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() { setup: function() {
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {
'Purchase Receipt': 'Receipt', 'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Invoice', 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier', 'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment', 'Payment Entry': 'Payment',
} }
@@ -158,16 +164,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if (doc.docstatus === 1 && !doc.inter_company_order_reference) { if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this; let me = this;
frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => { let internal = me.frm.doc.is_internal_supplier;
let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier); if (internal) {
let internal = supplier.is_internal_supplier; let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" :
let disabled = supplier.disabled; "Inter Company Sales Order";
if (internal === 1 && disabled === 0) {
me.frm.add_custom_button("Inter Company Order", function() { me.frm.add_custom_button(button_label, function() {
me.make_inter_company_order(me.frm); me.make_inter_company_order(me.frm);
}, __('Create')); }, __('Create'));
} }
});
} }
} }
@@ -347,7 +353,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
make_purchase_receipt: function() { make_purchase_receipt: function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
frm: cur_frm frm: cur_frm,
freeze_message: __("Creating Purchase Receipt ...")
}) })
}, },
@@ -374,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} }
}) })

View File

@@ -134,6 +134,8 @@
"ref_sq", "ref_sq",
"column_break_74", "column_break_74",
"party_account_currency", "party_account_currency",
"is_internal_supplier",
"represents_company",
"inter_company_order_reference" "inter_company_order_reference"
], ],
"fields": [ "fields": [
@@ -1101,13 +1103,28 @@
{ {
"fieldname": "items_col_break", "fieldname": "items_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"label": "Is Internal Supplier"
},
{
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-03 16:46:44.229351", "modified": "2021-01-20 22:07:23.487138",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController):
if self.is_subcontracted == "Yes": if self.is_subcontracted == "Yes":
for item in self.items: for item in self.items:
if not item.bom: if not item.bom:
frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}")
.format(item.item_code, item.idx))) .format(item.item_code, item.idx))
def get_schedule_dates(self): def get_schedule_dates(self):
for d in self.get('items'): for d in self.get('items'):

View File

@@ -40,6 +40,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
@@ -726,13 +727,21 @@
"fieldname": "more_info_section_break", "fieldname": "more_info_section_break",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "More Information" "label": "More Information"
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-07 11:59:47.670951", "modified": "2021-01-30 21:44:41.816974",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -224,7 +224,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} }
}) })
@@ -280,7 +280,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99] per_ordered: ["<", 100]
} }
}); });
dialog.hide(); dialog.hide();

View File

@@ -52,7 +52,10 @@ class Supplier(TransactionBase):
self.validate_internal_supplier() self.validate_internal_supplier()
def validate_internal_supplier(self): def validate_internal_supplier(self):
if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): internal_supplier = frappe.db.get_value("Supplier",
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
if internal_supplier:
frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.throw(_("Internal Supplier for company {0} already exists").format(
frappe.bold(self.represents_company))) frappe.bold(self.represents_company)))

View File

@@ -44,7 +44,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 100],
company: me.frm.doc.company company: me.frm.doc.company
} }
}) })

View File

@@ -75,62 +75,70 @@ frappe.query_reports["Purchase Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { events: {
onCheckRow: function(data) { onCheckRow: function (data) {
if (!data) return;
const data_doctype = $(
data[2].html
)[0].attributes.getNamedItem("data-doctype").value;
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
row_name = data[2].content; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Supplier") {
row_values = data
if(tree_type == "Supplier" || tree_type == "Item") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} } else if (tree_type == "Item") {
else { row_values = data
row_values = data.slice(3,length-1).map(function (column) { .slice(5, length - 1)
return column.content; .map(function (column) {
}) return column.content;
});
} else {
row_values = data
.slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
},500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}); });
} }
} }

View File

@@ -35,9 +35,10 @@ def update_last_purchase_rate(doc, is_submit):
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate # update last purchsae rate
if last_purchase_rate: frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
(flt(last_purchase_rate), d.item_code))
def validate_for_items(doc): def validate_for_items(doc):
items = [] items = []

View File

@@ -0,0 +1,77 @@
### Version 13.0.0 Beta 11 Release Notes
#### Features and Enhancements
- Provision to disable serial no and batch selector ([#24398](https://github.com/frappe/erpnext/pull/24398))
- Multi currency in landed cost voucher ([#24127](https://github.com/frappe/erpnext/pull/24127))
- Putaway ([#23969](https://github.com/frappe/erpnext/pull/23969))
- Item valuation for internal stock transfers ([#24200](https://github.com/frappe/erpnext/pull/24200))
- Batch wise item pricing ([#24470](https://github.com/frappe/erpnext/pull/24470))
- Project template with dependent tasks ([#24092](https://github.com/frappe/erpnext/pull/24092))
- Patient History Enhancements ([#24033](https://github.com/frappe/erpnext/pull/24033))
- Compute Year to Date for Salary Slip components ([#24362](https://github.com/frappe/erpnext/pull/24362))
- Configurable accounting dimension filters and validations ([#23912](https://github.com/frappe/erpnext/pull/23912))
- Issue Summary Script Report ([#23603](https://github.com/frappe/erpnext/pull/23603))
- Issue Analytics Script Report ([#23604](https://github.com/frappe/erpnext/pull/23604))
- Loan report and enhancements ([#24370](https://github.com/frappe/erpnext/pull/24370))
- Enhancements to erpnext membership ([#23865](https://github.com/frappe/erpnext/pull/23865))
- Allow Discharge despite Unbilled Healthcare Services ([#24281](https://github.com/frappe/erpnext/pull/24281))
- Patient appointment status changes ([#24201](https://github.com/frappe/erpnext/pull/24201))
- Allow selecting admission service unit in Patient Appointment for inpatients ([#24410](https://github.com/frappe/erpnext/pull/24410))
- Separate equity tree in CoA SKR04 ([#24095](https://github.com/frappe/erpnext/pull/24095))
- Do Not Bill Patient Encounters for Inpatients ([#24355](https://github.com/frappe/erpnext/pull/24355))
- Value Based and Numeric Quality Inspection ([#24181](https://github.com/frappe/erpnext/pull/24181))
- Deleting account & stock entries on deletion of transaction ([#24298](https://github.com/frappe/erpnext/pull/24298))
- Remove german sales invoice validation ([#24441](https://github.com/frappe/erpnext/pull/24441))
- Voice Call Settings doctype added ([#24126](https://github.com/frappe/erpnext/pull/24126))
- Shopping portal changes ([#24445](https://github.com/frappe/erpnext/pull/24445))
- Add "Sync Now" to Plaid Settings ([#23602](https://github.com/frappe/erpnext/pull/23602))
#### Fixes
- Multiple pricing rule with margin type as Percentage is not working ([#24204](https://github.com/frappe/erpnext/pull/24204))
- Allow statistical component in salary structure. ([#24424](https://github.com/frappe/erpnext/pull/24424))
- Set current asset value before calculating difference amount ([#24119](https://github.com/frappe/erpnext/pull/24119))
- To use Stock UoM in BOM Stock Report ([#24339](https://github.com/frappe/erpnext/pull/24339))
- Accounting entries of asset when submitting purchase receipt ([#24191](https://github.com/frappe/erpnext/pull/24191))
- Cancelling of asset value adjustement ([#24193](https://github.com/frappe/erpnext/pull/24193))
- Batch/Serial Selector for Scanned Batched Item ([#24338](https://github.com/frappe/erpnext/pull/24338))
- Link timesheets with corresponding projects ([#24346](https://github.com/frappe/erpnext/pull/24346))
- Material request wrong status issue ([#24019](https://github.com/frappe/erpnext/pull/24019))
- UX issues in e-invoicing ([#24358](https://github.com/frappe/erpnext/pull/24358))
- Company Wise Valuation Rate for RM in BOM ([#24324](https://github.com/frappe/erpnext/pull/24324))
- Stock ageing should not take cancelled stock entries. ([#24437](https://github.com/frappe/erpnext/pull/24437))
- Partial loan security unpledging ([#24252](https://github.com/frappe/erpnext/pull/24252))
- Asset depreciation ledger ([#24226](https://github.com/frappe/erpnext/pull/24226))
- Back Update from QC based on Batch No ([#24329](https://github.com/frappe/erpnext/pull/24329))
- Fix for not having fiscal year while creating new company ([#24130](https://github.com/frappe/erpnext/pull/24130))
- E-invoice print format not showing other charges ([#24474](https://github.com/frappe/erpnext/pull/24474))
- Tax template update on customer address change ([#24146](https://github.com/frappe/erpnext/pull/24146))
- Do not manufacture same serial no multiple times ([#24164](https://github.com/frappe/erpnext/pull/24164))
- Ignore group cost center validation for period closing voucher ([#24375](https://github.com/frappe/erpnext/pull/24375))
- Partial serial no return issue ([#24207](https://github.com/frappe/erpnext/pull/24207))
- GSTR-1 double entry issue ([#24376](https://github.com/frappe/erpnext/pull/24376))
- Not able to create dunning from sales invoice ([#24349](https://github.com/frappe/erpnext/pull/24349))
- Set company in leave allocation and leave ledger entry ([#24296](https://github.com/frappe/erpnext/pull/24296))
- Allow leave policy assignment to be canceled. ([#24265](https://github.com/frappe/erpnext/pull/24265))
- Removed all day event from shift assignment calendar ([#24397](https://github.com/frappe/erpnext/pull/24397))
- Tax calculation on salary slip for the first month ([#24272](https://github.com/frappe/erpnext/pull/24272))
- Validate tax template for tax category ([#24402](https://github.com/frappe/erpnext/pull/24402))
- Numeric/Non-numeric QI UX ([#24517](https://github.com/frappe/erpnext/pull/24517))
- Finished good produced qty validation ([#24220](https://github.com/frappe/erpnext/pull/24220))
- Incorrect serial no in the subcontracted purchase receipt ([#24354](https://github.com/frappe/erpnext/pull/24354))
- Don't validate warehouse values between Material Request and Stock Entry ([#24294](https://github.com/frappe/erpnext/pull/24294))
- Don't cancel job card if manufacturing entry has made ([#24063](https://github.com/frappe/erpnext/pull/24063))
- Subscription prepaid date validation ([#24356](https://github.com/frappe/erpnext/pull/24356))
- Allow addition and removal of employee in payroll Entry ([#24169](https://github.com/frappe/erpnext/pull/24169))
- Filter Therapy Types and Therapy Plan in Patient Appointment ([#24152](https://github.com/frappe/erpnext/pull/24152))
- Payment Period based on invoice date report fix/refactor ([#24378](https://github.com/frappe/erpnext/pull/24378))
- Drop ship partial order fixed ([#24072](https://github.com/frappe/erpnext/pull/24072))
- E-invoicing qrcode image generation ([#24395](https://github.com/frappe/erpnext/pull/24395))
- Payment entry multi-currency issue ([#24332](https://github.com/frappe/erpnext/pull/24332))
- Multiple pricing rule issue ([#24515](https://github.com/frappe/erpnext/pull/24515))
- Last purchase rate not updating when voucher cancelled if only one voucher is present ([#24322](https://github.com/frappe/erpnext/pull/24322))
- Do not cancel reference document on Quality Inspection cancellation ([#24197](https://github.com/frappe/erpnext/pull/24197))
- Refactored fetching & validating address from erpnext rather than gst portal ([#24297](https://github.com/frappe/erpnext/pull/24297))
- Opportunity Status fix ([#22944](https://github.com/frappe/erpnext/pull/22944))
- Extra transferred qty has not consumed against work order ([#24495](https://github.com/frappe/erpnext/pull/24495))

View File

@@ -0,0 +1,14 @@
### Version 13.0.0 Beta 12 Release Notes
#### Features
- Department wise Appointment Type charges ([#24572](https://github.com/frappe/erpnext/pull/24572))
- Capture Rate of stock UOM in purchase ([#24315](https://github.com/frappe/erpnext/pull/24315))
#### Fixes
- Fixed stock and account balance syncing ([#24644](https://github.com/frappe/erpnext/pull/24644))
- Fixed incorrect stock ledger qty in the stock ledger report and bin ([#24649](https://github.com/frappe/erpnext/pull/24649))
- Added patch to fix incorrect stock ledger and stock account value ([#24702](https://github.com/frappe/erpnext/pull/24702))
- Skip e-invoice generation for non-taxable invoices ([#24568](https://github.com/frappe/erpnext/pull/24568))
- Cannot cancel old invoices if eligible for e-invoicing ([#24608](https://github.com/frappe/erpnext/pull/24608))
- Mpesa fixes and enhancement ([#24306](https://github.com/frappe/erpnext/pull/24306))
- Fixed Consolidated Financial Statement report ([#24580](https://github.com/frappe/erpnext/pull/24580))

View File

@@ -75,6 +75,9 @@ class AccountsController(TransactionBase):
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year() self.validate_date_with_fiscal_year()
self.validate_inter_company_reference()
self.set_incoming_rate()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
@@ -119,6 +122,12 @@ class AccountsController(TransactionBase):
def before_cancel(self): def before_cancel(self):
validate_einvoice_fields(self) validate_einvoice_fields(self)
def on_trash(self):
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name))
frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name))
def validate_deferred_start_and_end_date(self): def validate_deferred_start_and_end_date(self):
for d in self.items: for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
@@ -206,6 +215,17 @@ class AccountsController(TransactionBase):
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self) self.meta.get_label(date_field), self)
def validate_inter_company_reference(self):
if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
return
if self.is_internal_transfer():
if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference')
or self.get('inter_company_order_reference')):
msg = _("Internal Sale or Delivery Reference missing. ")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
def validate_due_date(self): def validate_due_date(self):
if self.get('is_pos'): return if self.get('is_pos'): return
@@ -282,6 +302,7 @@ class AccountsController(TransactionBase):
args["doctype"] = self.doctype args["doctype"] = self.doctype
args["name"] = self.name args["name"] = self.name
args["child_docname"] = item.name args["child_docname"] = item.name
args["ignore_pricing_rule"] = self.ignore_pricing_rule if hasattr(self, 'ignore_pricing_rule') else 0
if not args.get("transaction_date"): if not args.get("transaction_date"):
args["transaction_date"] = args.get("posting_date") args["transaction_date"] = args.get("posting_date")
@@ -448,8 +469,10 @@ class AccountsController(TransactionBase):
account_currency = get_account_currency(gl_dict.account) account_currency = get_account_currency(gl_dict.account)
if gl_dict.account and self.doctype not in ["Journal Entry", if gl_dict.account and self.doctype not in ["Journal Entry",
"Period Closing Voucher", "Payment Entry"]: "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]:
self.validate_account_currency(gl_dict.account, account_currency) self.validate_account_currency(gl_dict.account, account_currency)
if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]:
set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"),
self.company_currency) self.company_currency)
@@ -962,9 +985,9 @@ class AccountsController(TransactionBase):
It will an internal transfer if its an internal customer and representation It will an internal transfer if its an internal customer and representation
company is same as billing company company is same as billing company
""" """
if self.doctype == 'Sales Invoice': if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'):
internal_party_field = 'is_internal_customer' internal_party_field = 'is_internal_customer'
else: elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
internal_party_field = 'is_internal_supplier' internal_party_field = 'is_internal_supplier'
if self.get(internal_party_field) and (self.represents_company == self.company): if self.get(internal_party_field) and (self.represents_company == self.company):
@@ -1481,6 +1504,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.flags.ignore_validate_update_after_submit = True parent.flags.ignore_validate_update_after_submit = True
parent.set_qty_as_per_stock_uom() parent.set_qty_as_per_stock_uom()
parent.calculate_taxes_and_totals() parent.calculate_taxes_and_totals()
parent.set_total_in_words()
if parent_doctype == "Sales Order": if parent_doctype == "Sales Order":
make_packing_list(parent) make_packing_list(parent)
parent.set_gross_profit() parent.set_gross_profit()

View File

@@ -44,7 +44,6 @@ class BuyingController(StockController):
self.validate_items() self.validate_items()
self.set_qty_as_per_stock_uom() self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items() self.validate_stock_or_nonstock_items()
self.update_tax_category_for_internal_transfer()
self.validate_warehouse() self.validate_warehouse()
self.validate_from_warehouse() self.validate_from_warehouse()
self.set_supplier_address() self.set_supplier_address()
@@ -100,11 +99,6 @@ class BuyingController(StockController):
msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
self.update_tax_category(msg) self.update_tax_category(msg)
def update_tax_category_for_internal_transfer(self):
if self.doctype == 'Purchase Invoice' and self.is_internal_transfer():
msg = _('Tax Category has been changed to "Total" as its an internal purchase.')
self.update_tax_category(msg)
def update_tax_category(self, msg): def update_tax_category(self, msg):
tax_for_valuation = [d for d in self.get("taxes") tax_for_valuation = [d for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]] if d.category in ["Valuation", "Valuation and Total"]]
@@ -224,6 +218,48 @@ class BuyingController(StockController):
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0
def set_incoming_rate(self):
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
return
ref_doctype_map = {
"Purchase Order": "Sales Order Item",
"Purchase Receipt": "Delivery Note Item",
"Purchase Invoice": "Sales Invoice Item",
}
ref_doctype = ref_doctype_map.get(self.doctype)
items = self.get("items")
for d in items:
if not cint(self.get("is_return")):
# Get outgoing rate based on original item cost based on valuation method
if not d.get(frappe.scrub(ref_doctype)):
outgoing_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.get('from_warehouse'),
"posting_date": self.get('posting_date') or self.get('transation_date'),
"posting_time": self.get('posting_time'),
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.get('serial_no'),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False)
rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate'))
else:
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate')
if self.is_internal_transfer():
if rate != d.rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
.format(d.idx), alert=1)
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0 supplied_items_cost = 0.0
for d in self.get("supplied_items"): for d in self.get("supplied_items"):
@@ -252,7 +288,7 @@ class BuyingController(StockController):
if self.is_subcontracted == "Yes": if self.is_subcontracted == "Yes":
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
frappe.throw(_("Supplier Warehouse mandatory for sub-contracted Purchase Receipt")) frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"): for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom: if item in self.sub_contracted_items and not item.bom:
@@ -336,7 +372,7 @@ class BuyingController(StockController):
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0) consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_nos', '') consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '') consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty transferred_qty = raw_material.qty
@@ -559,6 +595,8 @@ class BuyingController(StockController):
from_warehouse_sle = self.get_sl_entries(d, { from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty, "actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse, "warehouse": d.from_warehouse,
"outgoing_rate": d.rate,
"recalculate_rate": 1,
"dependant_sle_voucher_detail_no": d.name "dependant_sle_voucher_detail_no": d.name
}) })

View File

@@ -493,6 +493,41 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
'company': filters.get("company", "") 'company': filters.get("company", "")
}) })
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
meta = frappe.get_meta(doctype)
if meta.is_tree:
query_filters.append(['is_group', '=', 0])
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
if txt:
query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
query_selector = 'in'
else:
query_selector = 'not in'
if len(dimension_filters['allowed_dimensions']) == 1:
dimensions = tuple(dimension_filters['allowed_dimensions'] * 2)
else:
dimensions = tuple(dimension_filters['allowed_dimensions'])
query_filters.append(['name', query_selector, dimensions])
output = frappe.get_all(doctype, filters=query_filters)
result = [d.name for d in output]
return [(d,) for d in set(result)]
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
@@ -620,6 +655,34 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters) return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, filters):
query = """
select name
from `tabHealthcare Service Unit`
where
is_group = 0
and company = {company}
and name like {txt}""".format(
company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt)))
if filters and filters.get('inpatient_record'):
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record'))
# if the patient is admitted, then appointments should be allowed against the admission service unit,
# inspite of it being an Inpatient Occupancy service unit
if service_unit:
query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit))
else:
query += " and allow_appointments = 1"
else:
query += " and allow_appointments = 1"
return frappe.db.sql(query, filters)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters): def get_tax_template(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -262,6 +262,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
if doc.get("is_return"): if doc.get("is_return"):
if doc.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice': if doc.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice':
doc.consolidated_invoice = ""
doc.set('payments', []) doc.set('payments', [])
for data in source.payments: for data in source.payments:
paid_amount = 0.00 paid_amount = 0.00
@@ -328,6 +329,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
@@ -353,6 +355,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.dn_detail = source_doc.dn_detail target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name target_doc.sales_invoice_item = source_doc.name
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@@ -3,7 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
from frappe import _, throw from frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@@ -49,7 +49,6 @@ class SellingController(StockController):
self.set_customer_address() self.set_customer_address()
self.validate_for_duplicate_items() self.validate_for_duplicate_items()
self.validate_target_warehouse() self.validate_target_warehouse()
self.set_incoming_rate()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
@@ -233,7 +232,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate 'incoming_rate': p.get("incoming_rate")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@@ -252,7 +251,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate 'incoming_rate': d.get("incoming_rate")
})) }))
return il return il
@@ -312,7 +311,7 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows) sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self): def set_incoming_rate(self):
if self.doctype not in ("Delivery Note", "Sales Invoice"): if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"):
return return
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
@@ -322,15 +321,26 @@ class SellingController(StockController):
d.incoming_rate = get_incoming_rate({ d.incoming_rate = get_incoming_rate({
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.warehouse, "warehouse": d.warehouse,
"posting_date": self.posting_date, "posting_date": self.get('posting_date') or self.get('transaction_date'),
"posting_time": self.posting_time, "posting_time": self.get('posting_time') or nowtime(),
"qty": -1*flt(d.qty), "qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')),
"serial_no": d.serial_no, "serial_no": d.get('serial_no'),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation") "allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False) }, raise_error_if_no_rate=False)
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
.format(d.idx), alert=1)
elif self.get("return_against"): elif self.get("return_against"):
# Get incoming rate of return entry from reference document # Get incoming rate of return entry from reference document
# based on original item cost as per valuation method # based on original item cost as per valuation method
@@ -446,9 +456,13 @@ class SellingController(StockController):
check_list, chk_dupl_itm = [], [] check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")): if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return return
if self.doctype == "Sales Invoice" and self.is_consolidated:
return
if self.doctype == "POS Invoice":
return
for d in self.get('items'): for d in self.get('items'):
if self.doctype in ["POS Invoice","Sales Invoice"]: if self.doctype == "Sales Invoice":
stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note] non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note": elif self.doctype == "Delivery Note":
@@ -459,13 +473,19 @@ class SellingController(StockController):
non_stock_items = [d.item_code, d.description] non_stock_items = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
duplicate_items_msg += "<br><br>"
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
get_link_to_form("Selling Settings", "Selling Settings")
)
if stock_items in check_list: if stock_items in check_list:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) frappe.throw(duplicate_items_msg)
else: else:
check_list.append(stock_items) check_list.append(stock_items)
else: else:
if non_stock_items in chk_dupl_itm: if non_stock_items in chk_dupl_itm:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) frappe.throw(duplicate_items_msg)
else: else:
chk_dupl_itm.append(non_stock_items) chk_dupl_itm.append(non_stock_items)

View File

@@ -93,6 +93,12 @@ status_map = {
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"], ["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"], ["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
["Cancelled", "eval:self.docstatus == 2"], ["Cancelled", "eval:self.docstatus == 2"],
],
"POS Closing Entry": [
["Draft", None],
["Submitted", "eval:self.docstatus == 1"],
["Queued", "eval:self.status == 'Queued'"],
["Cancelled", "eval:self.docstatus == 2"],
] ]
} }

View File

@@ -6,6 +6,7 @@ import frappe, erpnext
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _ from frappe import _
import frappe.defaults import frappe.defaults
from collections import defaultdict
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@@ -23,6 +24,9 @@ class StockController(AccountsController):
self.validate_inspection() self.validate_inspection()
self.validate_serialized_batch() self.validate_serialized_batch()
self.validate_customer_provided_item() self.validate_customer_provided_item()
self.set_rate_of_stock_uom()
self.validate_internal_transfer()
self.validate_putaway_capacity()
def make_gl_entries(self, gl_entries=None, from_repost=False): def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2: if self.docstatus == 2:
@@ -70,8 +74,9 @@ class StockController(AccountsController):
gl_list = [] gl_list = []
warehouse_with_no_account = [] warehouse_with_no_account = []
precision = frappe.get_precision("GL Entry", "debit_in_account_currency") precision = self.get_debit_field_precision()
for item_row in voucher_details: for item_row in voucher_details:
sle_list = sle_map.get(item_row.name) sle_list = sle_map.get(item_row.name)
if sle_list: if sle_list:
for sle in sle_list: for sle in sle_list:
@@ -126,7 +131,13 @@ class StockController(AccountsController):
if frappe.db.get_value("Warehouse", wh, "company"): if frappe.db.get_value("Warehouse", wh, "company"):
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
return process_gl_map(gl_list) return process_gl_map(gl_list, precision=precision)
def get_debit_field_precision(self):
if not frappe.flags.debit_field_precision:
frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle): def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
@@ -216,7 +227,7 @@ class StockController(AccountsController):
""", (self.doctype, self.name), as_dict=True) """, (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries: for sle in stock_ledger_entries:
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger return stock_ledger
def make_batches(self, warehouse_field): def make_batches(self, warehouse_field):
@@ -239,7 +250,7 @@ class StockController(AccountsController):
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing")) .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else: else:
is_expense_account = frappe.db.get_value("Account", is_expense_account = frappe.get_cached_value("Account",
item.get("expense_account"), "report_type")=="Profit and Loss" item.get("expense_account"), "report_type")=="Profit and Loss"
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account: if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account:
frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account") frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account")
@@ -391,6 +402,89 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 d.allow_zero_valuation_rate = 1
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
and self.is_internal_transfer():
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
def validate_in_transit_warehouses(self):
if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note':
for item in self.get('items'):
if not item.target_warehouse:
frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx))
if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt':
for item in self.get('items'):
if not item.from_warehouse:
frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx))
def validate_multi_currency(self):
if self.currency != self.company_currency:
frappe.throw(_("Internal transfers can only be done in company's default currency"))
def validate_packed_items(self):
if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'):
frappe.throw(_("Packed Items cannot be transferred internally"))
def validate_putaway_capacity(self):
# if over receipt is attempted while 'apply putaway rule' is disabled
# and if rule was applied on the transaction, validate it.
from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity
valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice",
"Stock Reconciliation")
if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
valid_doctype = False
if valid_doctype:
rule_map = defaultdict(dict)
for item in self.get("items"):
warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse"
rule = frappe.db.get_value("Putaway Rule",
{
"item_code": item.get("item_code"),
"warehouse": item.get(warehouse_field)
},
["name", "disable"], as_dict=True)
if rule:
if rule.get("disabled"): continue # dont validate for disabled rule
if self.doctype == "Stock Reconciliation":
stock_qty = flt(item.qty)
else:
stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty)
rule_name = rule.get("name")
if not rule_map[rule_name]:
rule_map[rule_name]["warehouse"] = item.get(warehouse_field)
rule_map[rule_name]["item"] = item.get("item_code")
rule_map[rule_name]["qty_put"] = 0
rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name)
rule_map[rule_name]["qty_put"] += flt(stock_qty)
for rule, values in rule_map.items():
if flt(values["qty_put"]) > flt(values["capacity"]):
message = self.prepare_over_receipt_message(rule, values)
frappe.throw(msg=message, title=_("Over Receipt"))
def prepare_over_receipt_message(self, rule, values):
message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \
.format(
frappe.bold(values["qty_put"]), frappe.bold(values["item"]),
frappe.bold(values["warehouse"]), frappe.bold(values["capacity"])
)
message += "<br><br>"
rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule)
message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link)
return message
def repost_future_sle_and_gle(self): def repost_future_sle_and_gle(self):
args = frappe._dict({ args = frappe._dict({
"posting_date": self.posting_date, "posting_date": self.posting_date,
@@ -399,7 +493,6 @@ class StockController(AccountsController):
"voucher_no": self.name, "voucher_no": self.name,
"company": self.company "company": self.company
}) })
if check_if_future_sle_exists(args): if check_if_future_sle_exists(args):
create_repost_item_valuation_entry(args) create_repost_item_valuation_entry(args)
elif not is_reposting_pending(): elif not is_reposting_pending():

View File

@@ -10,6 +10,7 @@ from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc):
@@ -106,7 +107,7 @@ class calculate_taxes_and_totals(object):
elif item.discount_amount and item.pricing_rules: elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount item.rate = item.price_list_rate - item.discount_amount
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item']: if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0: if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
@@ -615,7 +616,6 @@ class calculate_taxes_and_totals(object):
self.doc.precision("base_write_off_amount")) self.doc.precision("base_write_off_amount"))
def calculate_margin(self, item): def calculate_margin(self, item):
rate_with_margin = 0.0 rate_with_margin = 0.0
base_rate_with_margin = 0.0 base_rate_with_margin = 0.0
if item.price_list_rate: if item.price_list_rate:
@@ -624,8 +624,8 @@ class calculate_taxes_and_totals(object):
for d in get_applied_pricing_rules(item.pricing_rules): for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if (pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == self.doc.currency)\ if pricing_rule.margin_rate_or_amount and ((pricing_rule.currency == self.doc.currency and
or (pricing_rule.margin_type == 'Percentage'): pricing_rule.margin_type in ['Amount', 'Percentage']) or pricing_rule.margin_type == 'Percentage'):
item.margin_type = pricing_rule.margin_type item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
has_margin = True has_margin = True
@@ -758,3 +758,35 @@ def get_rounded_tax_amount(itemised_tax, precision):
for taxes in itemised_tax.values(): for taxes in itemised_tax.values():
for tax_account in taxes: for tax_account in taxes:
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
class init_landed_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs'
self.set_account_currency()
self.set_exchange_rate()
self.set_amounts_in_company_currency()
def set_account_currency(self):
company_currency = erpnext.get_company_currency(self.doc.company)
for d in self.doc.get(self.tax_field):
if not d.account_currency:
account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency')
d.account_currency = account_currency or company_currency
def set_exchange_rate(self):
company_currency = erpnext.get_company_currency(self.doc.company)
for d in self.doc.get(self.tax_field):
if d.account_currency == company_currency:
d.exchange_rate = 1
elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date:
d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account,
account_currency=d.account_currency, company=self.doc.company)
if not d.exchange_rate:
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
def set_amounts_in_company_currency(self):
for d in self.doc.get(self.tax_field):
d.amount = flt(d.amount, d.precision("amount"))
d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))

View File

@@ -6,6 +6,7 @@ import unittest
from erpnext.stock.doctype.item.test_item import set_item_variant_settings from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter
from six import string_types from six import string_types
@@ -56,6 +57,8 @@ def make_quality_inspection_template():
qc = frappe.new_doc("Quality Inspection Template") qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template qc.quality_inspection_template_name = qc_template
create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', { qc.append('item_quality_inspection_parameter', {
"specification": "Moisture", "specification": "Moisture",
"value": "&lt; 5%", "value": "&lt; 5%",

View File

@@ -126,7 +126,7 @@ class Appointment(Document):
add_assignemnt({ add_assignemnt({
'doctype': self.doctype, 'doctype': self.doctype,
'name': self.name, 'name': self.name,
'assign_to': existing_assignee 'assign_to': [existing_assignee]
}) })
return return
if self._assign: if self._assign:
@@ -139,7 +139,7 @@ class Appointment(Document):
add_assignemnt({ add_assignemnt({
'doctype': self.doctype, 'doctype': self.doctype,
'name': self.name, 'name': self.name,
'assign_to': agent 'assign_to': [agent]
}) })
break break

Some files were not shown because too many files have changed in this diff Show More