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

This commit is contained in:
Deepesh Garg
2021-03-13 23:18:13 +05:30
381 changed files with 10963 additions and 3591 deletions

View File

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

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.0.0-beta.7'
__version__ = '13.0.0-beta.13'
def get_default_company(user=None):
'''Get default company for user'''
@@ -132,16 +132,10 @@ def allow_regional(fn):
return caller
def get_last_membership():
def get_last_membership(member):
'''Returns last membership if exists'''
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]
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
if last_membership:
return last_membership[0]

View File

@@ -910,98 +910,8 @@
},
"is_group": 1
},
"Passiva": {
"Passiva - Verbindlichkeiten": {
"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": {
"is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@@ -1618,6 +1528,143 @@
},
"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": {
"root_type": "Income",
"is_group": 1,

View File

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

View File

@@ -33,11 +33,11 @@ class AccountingDimension(Document):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
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):
if frappe.flags.in_test:
delete_accounting_dimension(doc=self)
delete_accounting_dimension(doc=self, queue='long')
else:
frappe.enqueue(delete_accounting_dimension, doc=self)
@@ -48,6 +48,9 @@ class AccountingDimension(Document):
if not self.fieldname:
self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc):
doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions())
@@ -165,9 +168,9 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype)
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",
"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",
"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",
@@ -176,12 +179,14 @@ def get_doctypes_with_dimensions():
return doclist
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:
return [d.fieldname for d in accounting_dimensions]
return [d.fieldname for d in frappe.flags.accounting_dimensions]
else:
return accounting_dimensions
return frappe.flags.accounting_dimensions
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

View File

@@ -6,6 +6,18 @@ frappe.ui.form.on('Accounting Dimension Filter', {
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() {

View File

@@ -14,7 +14,9 @@
"section_break_4",
"accounts",
"column_break_6",
"dimensions"
"dimensions",
"section_break_10",
"dimension_filter_help"
],
"fields": [
{
@@ -89,11 +91,24 @@
"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": "2020-11-24 12:34:42.458713",
"modified": "2021-02-03 12:04:58.678402",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
@@ -110,6 +125,30 @@
"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,

View File

@@ -13,17 +13,21 @@ 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
@@ -32,11 +36,17 @@ class TestAccountingDimensionFilter(unittest.TestCase):
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'}):
@@ -71,7 +81,7 @@ def create_accounting_dimension_filter():
}],
'dimensions': [{
'accounting_dimension': 'Department',
'dimension_value': '_Test Department - _TC'
'dimension_value': 'Accounts - _TC'
}]
}).insert()
else:

View File

@@ -22,6 +22,7 @@
"book_asset_depreciation_entry_automatically",
"add_taxes_from_item_tax_template",
"automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"deferred_accounting_settings_section",
"automatically_process_deferred_accounting_entry",
"book_deferred_entries_based_on",
@@ -268,4 +269,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View File

@@ -1,5 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', {
onload: function(frm) {
@@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', {
frm.set_df_property('address_and_contact', 'hidden', 0);
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);
});
}
}
});
@@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) {
frm.doc.name).options = options;
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

@@ -11,7 +11,7 @@ frappe.ui.form.on('Budget', {
report_type: "Profit and Loss",
is_group: 0
}
}
};
});
frm.set_query("monthly_distribution", function() {
@@ -19,7 +19,7 @@ frappe.ui.form.on('Budget', {
filters: {
fiscal_year: frm.doc.fiscal_year
}
}
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);

View File

@@ -122,8 +122,10 @@ class TestBudget(unittest.TestCase):
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",
"_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)
@@ -147,8 +149,11 @@ class TestBudget(unittest.TestCase):
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",
"_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)
@@ -159,10 +164,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
month = now_datetime().month
if month > 10:
month = 10
if month > 9:
month = 9
for i in range(month):
for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_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")
month = now_datetime().month
if month > 10:
month = 10
if month > 9:
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",
"_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",
{"voucher_type": "Journal Entry", "voucher_no": jv.name}))
@@ -289,7 +296,7 @@ def make_budget(**args):
budget = frappe.new_doc("Budget")
if budget_against == "Project":
budget.project = "_Test Project"
budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else:
budget.cost_center =cost_center or "_Test Cost Center - _TC"

View File

@@ -27,30 +27,30 @@ class GLEntry(Document):
def validate(self):
self.flags.ignore_submit_comment = True
self.check_mandatory()
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
self.validate_cost_center()
if not self.flags.from_repost:
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account()
self.validate_party()
self.validate_currency()
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
if not from_repost:
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
self.validate_account_details(adv_adj)
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)
validate_balance_type(self.account, adv_adj)
# Update outstanding amt on against voucher
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
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)
# Update outstanding amt on against voucher
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(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher)
def check_mandatory(self):
mandatory = ['account','voucher_type','voucher_no','company']
@@ -58,7 +58,7 @@ class GLEntry(Document):
if not self.get(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 account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
@@ -73,7 +73,7 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account))
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':
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))
@@ -107,12 +107,12 @@ class GLEntry(Document):
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 account {1}").format(
frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
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 account {1}").format(
frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
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):
if self.is_opening=='Yes' and \
@@ -140,26 +140,18 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account, self.company))
def validate_cost_center(self):
if not hasattr(self, "cost_center_company"):
self.cost_center_company = {}
if not self.cost_center: return
def _get_cost_center_company():
if not self.cost_center_company.get(self.cost_center):
self.cost_center_company[self.cost_center] = frappe.db.get_value(
"Cost Center", self.cost_center, "company")
is_group, company = frappe.get_cached_value('Cost Center',
self.cost_center, ['is_group', 'company'])
return self.cost_center_company[self.cost_center]
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:
if company != self.company:
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))
if self.cost_center and _check_is_group():
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
.format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
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""").format(
self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
@@ -169,7 +161,7 @@ class GLEntry(Document):
account_currency = get_account_currency(self.account)
if not self.account_currency:
self.account_currency = company_currency
self.account_currency = account_currency or company_currency
if account_currency != self.account_currency:
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")
@@ -183,7 +175,6 @@ class GLEntry(Document):
if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
def validate_balance_type(account, adv_adj=False):
if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
@@ -249,7 +240,7 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
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:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
'frozen_accounts_modifier')

View File

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

View File

@@ -160,7 +160,7 @@ class TestJournalEntry(unittest.TestCase):
self.assertFalse(gle)
def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False)
@@ -299,15 +299,20 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project
project = make_project({
'project_name': 'Journal Entry Project',
'project_template_name': 'Test Project Template',
'start_date': '2020-01-01'
})
if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
project = make_project({
'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)
for d in jv.accounts:
d.project = project.project_name
d.project = project_name
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
jv.cheque_no = "112233"
@@ -317,10 +322,10 @@ class TestJournalEntry(unittest.TestCase):
expected_values = {
"_Test Cash - _TC": {
"project": project.project_name
"project": project_name
},
"_Test Bank - _TC": {
"project": project.project_name
"project": project_name
}
}

View File

@@ -396,6 +396,8 @@ frappe.ui.form.on('Payment Entry', {
set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@@ -422,6 +424,14 @@ frappe.ui.form.on('Payment Entry', {
if(!frm.doc.paid_amount && frm.doc.received_amount)
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;
}
}
}
},
() => {

View File

@@ -3,6 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import frappe
from frappe import _
from frappe.model.document import Document
@@ -82,18 +83,37 @@ class PaymentRequest(Document):
self.make_communication_entry()
elif self.payment_channel == "Phone":
controller = get_payment_gateway_controller(self.payment_gateway)
payment_record = dict(
reference_doctype="Payment Request",
reference_docname=self.name,
payment_reference=self.reference_name,
grand_total=self.grand_total,
sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
self.request_phone_payment()
def request_phone_payment(self):
controller = get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()
payment_record = dict(
reference_doctype="Payment Request",
reference_docname=self.name,
payment_reference=self.reference_name,
request_amount=request_amount,
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):
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:
pr.flags.mute_email = True
pr.insert(ignore_permissions=True)
if args.submit_doc:
pr.insert(ignore_permissions=True)
pr.submit()
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):
"""return gateway and payment account of default payment gateway"""
if args.get("payment_gateway"):
return get_payment_gateway_account(args.get("payment_gateway"))
if args.get("payment_gateway_account"):
return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart":
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):
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_name, so_inr.name)
@@ -54,7 +55,8 @@ class TestPaymentRequest(unittest.TestCase):
conversion_rate = get_exchange_rate("USD", "INR")
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_name, si_usd.name)
@@ -68,7 +70,7 @@ class TestPaymentRequest(unittest.TestCase):
so_inr = make_sales_order(currency="INR")
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()
so_inr = frappe.get_doc("Sales Order", so_inr.name)
@@ -79,7 +81,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50)
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()
@@ -106,7 +108,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50)
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()
pr.load_from_db()

View File

@@ -3,6 +3,7 @@
frappe.ui.form.on('POS Closing Entry', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
frm.set_query("pos_profile", function(doc) {
return {
filters: { 'user': doc.user }
@@ -20,7 +21,7 @@ frappe.ui.form.on('POS Closing Entry', {
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);
},

View File

@@ -11,6 +11,7 @@
"column_break_3",
"posting_date",
"pos_opening_entry",
"status",
"section_break_5",
"company",
"column_break_7",
@@ -184,11 +185,27 @@
"label": "POS Opening Entry",
"options": "POS Opening Entry",
"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,
"links": [],
"modified": "2020-05-29 15:03:22.226113",
"links": [
{
"link_doctype": "POS Invoice Merge Log",
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2021-01-12 12:21:05.388650",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",

View File

@@ -6,13 +6,12 @@ from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, get_datetime, flt
from collections import defaultdict
from frappe.utils import get_datetime, flt
from erpnext.controllers.status_updater import StatusUpdater
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):
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"))
@@ -57,20 +56,29 @@ class POSClosingEntry(Document):
if not invalid_rows:
return
error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows]
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
error_list = []
for row in invalid_rows:
for msg in row.get('msg'):
error_list.append(_("Row #{}: {}").format(row.get('idx'), msg))
def on_submit(self):
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()
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"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.validate_and_sanitize_search_inputs

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):
def test_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)
@@ -45,6 +44,49 @@ class TestPOSClosingEntry(unittest.TestCase):
frappe.set_user("Administrator")
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):
user = 'test@example.com'
test_user = frappe.get_doc('User', user)

View File

@@ -2,6 +2,7 @@
// For license information, please see license.txt
{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
setup(doc) {
@@ -9,12 +10,19 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
this._super(doc);
},
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload(doc) {
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') {
this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields();
}
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh(doc) {
@@ -187,18 +195,43 @@ frappe.ui.form.on('POS Invoice', {
},
request_for_payment: function (frm) {
if (!frm.doc.contact_mobile) {
frappe.throw(__('Please enter mobile number first.'));
}
frm.dirty();
frm.save().then(() => {
frappe.dom.freeze();
frappe.call({
method: 'create_payment_request',
doc: frm.doc,
})
frappe.dom.freeze(__('Waiting for payment...'));
frappe
.call({
method: 'create_payment_request',
doc: frm.doc
})
.fail(() => {
frappe.dom.unfreeze();
frappe.msgprint('Payment request failed');
frappe.msgprint(__('Payment request failed'));
})
.then(() => {
frappe.msgprint('Payment request sent successfully');
.then(({ message }) => {
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
from frappe import _
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.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.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
@@ -58,6 +57,22 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points()
self.check_phone_payments()
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):
# run on cancel method of selling controller
@@ -78,7 +93,7 @@ class POSInvoice(SalesInvoice):
mode_of_payment=pay.mode_of_payment, status="Paid"),
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))
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
if not self.pos_profile:
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')
profile = {}
@@ -294,14 +311,21 @@ class POSInvoice(SalesInvoice):
self.set(fieldname, profile.get(fieldname))
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')
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:
selling_price_list = profile.get('selling_price_list')
if 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
for item in self.get("items"):
@@ -361,22 +385,48 @@ class POSInvoice(SalesInvoice):
if not self.contact_mobile:
frappe.throw(_("Please enter the phone number first"))
payment_gateway = frappe.db.get_value("Payment Gateway Account", {
"payment_account": pay.account,
})
record = {
"payment_gateway": payment_gateway,
"dt": "POS Invoice",
"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
}
pay_req = self.get_existing_payment_request(pay)
if not pay_req:
pay_req = self.get_new_payment_request(pay)
pay_req.submit()
else:
pay_req.request_phone_payment()
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 append_payment(payment_mode):

View File

@@ -290,7 +290,7 @@ class TestPOSInvoice(unittest.TestCase):
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_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`")
test_user, pos_profile = init_user_and_profile()
@@ -306,7 +306,7 @@ class TestPOSInvoice(unittest.TestCase):
})
pos_inv2.submit()
merge_pos_invoices()
consolidate_pos_invoices()
pos_inv.load_from_db()
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):
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`")
test_user, pos_profile = init_user_and_profile()
@@ -348,7 +348,7 @@ class TestPOSInvoice(unittest.TestCase):
})
pos_inv2.submit()
merge_pos_invoices()
consolidate_pos_invoices()
pos_inv.load_from_db()
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):
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"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
@@ -393,7 +393,7 @@ class TestPOSInvoice(unittest.TestCase):
})
pos_inv2.submit()
merge_pos_invoices()
consolidate_pos_invoices()
pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")

View File

@@ -7,6 +7,8 @@
"field_order": [
"posting_date",
"customer",
"column_break_3",
"pos_closing_entry",
"section_break_3",
"pos_invoices",
"references_section",
@@ -76,11 +78,22 @@
"label": "Consolidated Credit Note",
"options": "Sales Invoice",
"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,
"links": [],
"modified": "2020-05-29 15:08:41.317100",
"modified": "2020-12-01 11:53:57.267579",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",

View File

@@ -5,10 +5,13 @@
from __future__ import unicode_literals
import frappe
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.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
@@ -61,7 +64,13 @@ class POSInvoiceMergeLog(Document):
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):
sales_invoice = self.get_new_sales_invoice()
@@ -83,7 +92,7 @@ class POSInvoiceMergeLog(Document):
credit_note.is_consolidated = 1
# 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.submit()
self.consolidated_credit_note = credit_note.name
@@ -111,7 +120,9 @@ class POSInvoiceMergeLog(Document):
i.qty = i.qty + item.qty
if not found:
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'):
found = False
@@ -147,6 +158,8 @@ class POSInvoiceMergeLog(Document):
invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
return invoice
@@ -159,17 +172,21 @@ class POSInvoiceMergeLog(Document):
return sales_invoice
def update_pos_invoices(self, sales_invoice, credit_note):
for d in self.pos_invoices:
doc = frappe.get_doc('POS Invoice', d.pos_invoice)
if not doc.is_return:
doc.update({'consolidated_invoice': sales_invoice})
else:
doc.update({'consolidated_invoice': credit_note})
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for doc in invoice_docs:
doc.load_from_db()
doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
doc.set_status(update=True)
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 = {
'consolidated_invoice': [ 'in', [ '', None ]],
'status': ['not in', ['Consolidated']],
@@ -180,7 +197,7 @@ def get_all_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 = {}
for invoice in pos_invoices:
@@ -190,20 +207,82 @@ def get_invoices_customer_map(pos_invoices):
return pos_invoice_customer_map
def merge_pos_invoices(pos_invoices=[]):
if not pos_invoices:
pos_invoices = get_all_invoices()
pos_invoice_map = get_invoices_customer_map(pos_invoices)
create_merge_logs(pos_invoice_map)
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices)
def create_merge_logs(pos_invoice_customer_map):
for customer, invoices in iteritems(pos_invoice_customer_map):
if len(invoices) >= 5 and closing_entry:
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 unconsolidate_pos_invoices(closing_entry):
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.posting_date = getdate(nowdate())
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
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
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_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
class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -34,7 +34,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
})
pos_inv3.submit()
merge_pos_invoices()
consolidate_pos_invoices()
pos_inv.load_from_db()
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.submit()
merge_pos_invoices()
consolidate_pos_invoices()
pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))

View File

@@ -6,7 +6,6 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater):

View File

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

View File

@@ -12,8 +12,6 @@
"company",
"country",
"column_break_9",
"update_stock",
"ignore_pricing_rule",
"warehouse",
"campaign",
"company_address",
@@ -25,8 +23,14 @@
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
"item_groups",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
"section_break_23",
"item_groups",
"column_break_25",
"customer_groups",
"section_break_16",
"print_format",
@@ -309,6 +313,7 @@
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
"hidden": 1,
"label": "Update Stock",
"read_only": 1
},
@@ -329,13 +334,34 @@
"fieldname": "auto_add_item_to_cart",
"fieldtype": "Check",
"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",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-20 13:59:28.877572",
"modified": "2021-01-06 14:42:41.713864",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -56,6 +56,7 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(details.get("discount_percentage"), 10)
prule = frappe.get_doc(test_record.copy())
prule.priority = 1
prule.applicable_for = "Customer"
prule.title = "_Test Pricing Rule for Customer"
self.assertRaises(MandatoryError, prule.insert)
@@ -261,6 +262,7 @@ class TestPricingRule(unittest.TestCase):
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 17.5,
"priority": 1,
"company": "_Test Company"
}).insert()
@@ -557,6 +559,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"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 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:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
if pricing_rule:
if isinstance(pricing_rule, list):
rules.extend(pricing_rule)
else:
rules.append(pricing_rule)
else:
pricing_rule = filter_pricing_rules(args, pricing_rules, doc)
@@ -53,17 +54,22 @@ def get_pricing_rules(args, doc=None):
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
pricing_rules_list = []
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):
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
@@ -144,9 +150,7 @@ def apply_multiple_pricing_rules(pricing_rules):
if not apply_multiple_rule: return False
if (apply_multiple_rule
and len(apply_multiple_rule) == len(pricing_rules)):
return True
return True
def _get_tree_conditions(args, parenttype, table, allow_blank=True):
field = frappe.scrub(parenttype)
@@ -264,18 +268,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if max_priority:
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):
pricing_rules = list(pricing_rules)

View File

@@ -508,7 +508,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {
'Purchase Invoice': 'Debit Note',
'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment'
}

View File

@@ -611,7 +611,8 @@ class PurchaseInvoice(BuyingController):
"against": item.expense_account,
"cost_center": item.cost_center,
"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
}, item=item))

View File

@@ -426,32 +426,37 @@ class TestPurchaseInvoice(unittest.TestCase):
)
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)
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
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project")
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000)
pi1 = make_purchase_invoice(qty=10, project="_Test Project")
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
pi1 = make_purchase_invoice(qty=10, project=project.name)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15500)
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)
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):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
@@ -860,17 +865,17 @@ class TestPurchaseInvoice(unittest.TestCase):
})
pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
pi.items[0].project = item_project.project_name
pi.project = project.project_name
pi.items[0].project = item_project.name
pi.project = project.name
pi.submit()
expected_values = {
"Creditors - _TC": {
"project": project.project_name
"project": project.name
},
"_Test Account Cost for Goods Sold - _TC": {
"project": item_project.project_name
"project": item_project.name
}
}
@@ -1026,7 +1031,7 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No"
if args.supplier_warehouse:
if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", {

View File

@@ -40,6 +40,7 @@
"base_rate",
"base_amount",
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"section_break_22",
"net_rate",
@@ -783,6 +784,14 @@
"print_hide": 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",
@@ -795,7 +804,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-12-26 17:20:36.415791",
"modified": "2021-01-30 21:43:21.488258",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -20,6 +20,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this;
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) {
// show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0);
@@ -574,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() {
return {
filters: {
@@ -595,7 +587,7 @@ frappe.ui.form.on('Sales Invoice', {
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return',
'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request',
'Payment Entry': 'Payment'
},

View File

@@ -1987,8 +1987,15 @@
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [],
"modified": "2020-12-25 22:57:32.555067",
"links": [
{
"custom": 1,
"group": "Reference",
"link_doctype": "POS Invoice",
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-01-12 12:16:15.192520",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
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 erpnext.accounts.party import get_party_account, get_due_date, get_party_details
from frappe.model.mapper import get_mapped_doc
@@ -212,6 +212,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
@@ -263,7 +266,25 @@ class SalesInvoice(SellingController):
if len(self.payments) == 0 and self.is_pos:
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):
self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None)
@@ -465,7 +486,9 @@ class SalesInvoice(SellingController):
if not for_validate and not self.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'):
self.account_for_change_amount = pos.get('account_for_change_amount')
@@ -1891,6 +1914,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""",
(company, mode_of_payment), as_dict=1)
@frappe.whitelist()
def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount

View File

@@ -1574,17 +1574,17 @@ class TestSalesInvoice(unittest.TestCase):
})
sales_invoice = create_sales_invoice(do_not_save=1)
sales_invoice.items[0].project = item_project.project_name
sales_invoice.project = project.project_name
sales_invoice.items[0].project = item_project.name
sales_invoice.project = project.name
sales_invoice.submit()
expected_values = {
"Debtors - _TC": {
"project": project.project_name
"project": project.name
},
"Sales - _TC": {
"project": item_project.project_name
"project": item_project.name
}
}
@@ -1885,23 +1885,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_json(self):
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.naming_series = 'INV-2020-.#####'
si.items = []
@@ -1909,8 +1892,8 @@ class TestSalesInvoice(unittest.TestCase):
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 2,
"rate": 100,
"qty": 2000,
"rate": 12,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
@@ -1919,31 +1902,52 @@ class TestSalesInvoice(unittest.TestCase):
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 4,
"rate": 150,
"qty": 420,
"rate": 15,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']])
total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']])
total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']])
total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']])
total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']])
total_item_ass_value = 0
total_item_cgst_value = 0
total_item_sgst_value = 0
total_item_igst_value = 0
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['ValDtls']['AssVal'], total_item_ass_value)
self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value)
self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value)
self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value)
self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value)
self.assertEqual(value_details['AssVal'], total_item_ass_value)
self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_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'])
def make_sales_invoice_for_ewaybill():
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
@@ -1992,6 +1996,7 @@ def make_sales_invoice_for_ewaybill():
address.save()
def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({
"doctype": "Supplier",
@@ -2002,12 +2007,17 @@ def make_sales_invoice_for_ewaybill():
"is_transporter": 1
}).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_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
filters = {"company": "_Test Company"})
filters = {"company": "_Test Company"}
)
if not gst_account:
gst_settings.append("gst_accounts", {
@@ -2019,7 +2029,7 @@ def make_sales_invoice_for_ewaybill():
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.company_address = "_Test Address for Eway bill-Billing"

View File

@@ -45,6 +45,7 @@
"base_rate",
"base_amount",
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"section_break_21",
"net_rate",
@@ -811,12 +812,20 @@
"no_copy": 1,
"print_hide": 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,
"istable": 1,
"links": [],
"modified": "2020-12-26 17:25:04.090630",
"modified": "2021-01-30 21:42:37.796771",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(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"):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc)
@@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc):
def validate_disabled(doc):
if doc.is_default and doc.disabled:
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

@@ -446,7 +446,7 @@ class Subscription(Document):
if not self.generate_invoice_at_period_start:
return False
if self.is_new_subscription():
if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True
# 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}")
.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:
gl_map = merge_similar_entries(gl_map)
gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
@@ -69,7 +69,7 @@ def process_gl_map(gl_map, merge_entries=True):
return gl_map
def merge_similar_entries(gl_map):
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
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_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
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.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost
gle.insert()
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or 'Yes'
gle.submit()
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 = 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)
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(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(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.TotInvVal, None, "INR") }}</td>
</tr>

View File

@@ -47,21 +47,22 @@ def get_data(filters):
for d in gl_entries:
asset_data = assets_details.get(d.against_voucher)
if not asset_data.get("accumulated_depreciation_amount"):
asset_data.accumulated_depreciation_amount = d.debit
else:
asset_data.accumulated_depreciation_amount += d.debit
if asset_data:
if not asset_data.get("accumulated_depreciation_amount"):
asset_data.accumulated_depreciation_amount = d.debit
else:
asset_data.accumulated_depreciation_amount += d.debit
row = frappe._dict(asset_data)
row.update({
"depreciation_amount": d.debit,
"depreciation_date": d.posting_date,
"amount_after_depreciation": (flt(row.gross_purchase_amount) -
flt(row.accumulated_depreciation_amount)),
"depreciation_entry": d.voucher_no
})
row = frappe._dict(asset_data)
row.update({
"depreciation_amount": d.debit,
"depreciation_date": d.posting_date,
"amount_after_depreciation": (flt(row.gross_purchase_amount) -
flt(row.accumulated_depreciation_amount)),
"depreciation_entry": d.voucher_no
})
data.append(row)
data.append(row)
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,
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)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
@@ -339,7 +339,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
return data
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], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company',
@@ -382,15 +382,31 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
for entry in gl_entries:
key = entry.account_number or entry.account_name
validate_entries(key, entry, accounts_by_name)
validate_entries(key, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry)
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:
field = "Account number" if entry.account_number else "Account name"
frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
args = get_account_details(entry.account)
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):
additional_conditions = []

View File

@@ -54,8 +54,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row = {
'item_code': d.item_code,
'item_name': item_record.item_name,
'item_group': item_record.item_group,
'item_name': item_record.item_name if item_record else d.item_name,
'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description,
'invoice': d.parent,
'posting_date': d.posting_date,
@@ -316,8 +316,10 @@ def get_items(filters, additional_query_columns):
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
`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`.unrealized_profit_loss_account,
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,

View File

@@ -59,23 +59,111 @@ def validate_filters(filters):
def get_columns(filters):
return [
_("Payment Document") + ":: 100",
_("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140",
_("Party Type") + "::100",
_("Party") + ":Dynamic Link/Party Type:140",
_("Posting Date") + ":Date: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",
_("Debit") + ":Currency:120",
_("Credit") + ":Currency:120",
_("Remarks") + "::150",
_("Age") +":Int:40",
"0-30:Currency:100",
"30-60:Currency:100",
"60-90:Currency:100",
_("90-Above") + ":Currency:100",
_("Delay in payment (Days)") + "::150"
{
"fieldname": "payment_document",
"label": _("Payment Document Type"),
"fieldtype": "Data",
"width": 100
},
{
"fieldname": "payment_entry",
"label": _("Payment Document"),
"fieldtype": "Dynamic Link",
"options": "payment_document",
"width": 160
},
{
"fieldname": "party_type",
"label": _("Party Type"),
"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):

View File

@@ -82,7 +82,7 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg)
@@ -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,
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):
frappe.db.sql("""delete from `tabGL Entry`
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:
warehouse_account = get_warehouse_account_map(company)
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items)
gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
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), [])
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)
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)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_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 = []
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)))
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
from `tabStock Ledger Entry` sle
where
@@ -945,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
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
for entry in expected_gle:
account_existed = False
for e in existing_gle:
if entry.account == e.account:
account_existed = True
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 (entry.debit != e.debit or entry.credit != e.credit):
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 ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False
break
if not account_existed:
@@ -982,7 +992,7 @@ def check_if_stock_and_account_balance_synced(posting_date, company, voucher_typ
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format(
stock_bal, account_bal, frappe.bold(account), posting_date)
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\
.format(frappe.bold(diff), frappe.bold(posting_date))
.format(frappe.bold(diff), frappe.bold(posting_date))
frappe.msgprint(
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),

View File

@@ -48,7 +48,7 @@ class CropCycle(Document):
def import_disease_tasks(self, disease, start_date):
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):
project = frappe.get_doc({

View File

@@ -71,4 +71,4 @@ def check_task_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

@@ -13,17 +13,14 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
class AssetValueAdjustment(Document):
def validate(self):
self.validate_date()
self.set_difference_amount()
self.set_current_asset_value()
self.set_difference_amount()
def on_submit(self):
self.make_depreciation_entry()
self.reschedule_depreciations(self.new_asset_value)
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)
def validate_date(self):

View File

@@ -64,8 +64,8 @@ frappe.ui.form.on("Purchase Order Item", {
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() {
this.frm.custom_make_buttons = {
'Purchase Receipt': 'Receipt',
'Purchase Invoice': 'Invoice',
'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment',
}
@@ -353,7 +353,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
make_purchase_receipt: function() {
frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
frm: cur_frm
frm: cur_frm,
freeze_message: __("Creating Purchase Receipt ...")
})
},
@@ -380,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99],
per_ordered: ["<", 100],
company: me.frm.doc.company
}
})

View File

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

View File

@@ -40,6 +40,7 @@
"base_rate",
"base_amount",
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"section_break_29",
"net_rate",
@@ -726,13 +727,21 @@
"fieldname": "more_info_section_break",
"fieldtype": "Section Break",
"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,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-07 11:59:47.670951",
"modified": "2021-01-30 21:44:41.816974",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

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

View File

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

View File

@@ -75,62 +75,70 @@ frappe.query_reports["Purchase Analytics"] = {
return Object.assign(options, {
checkboxColumn: true,
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;
length = data.length;
var tree_type = frappe.query_report.filters[0].value;
if(tree_type == "Supplier" || tree_type == "Item") {
row_values = data.slice(4,length-1).map(function (column) {
return column.content;
})
}
else {
row_values = data.slice(3,length-1).map(function (column) {
return column.content;
})
if (tree_type == "Supplier") {
row_values = data
.slice(4, length - 1)
.map(function (column) {
return column.content;
});
} else if (tree_type == "Item") {
row_values = data
.slice(5, length - 1)
.map(function (column) {
return column.content;
});
} else {
row_values = data
.slice(3, length - 1)
.map(function (column) {
return column.content;
});
}
entry = {
'name':row_name,
'values':row_values
}
entry = {
name: row_name,
values: row_values,
};
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
var found = false;
for(var i=0; i < new_datasets.length;i++){
if(new_datasets[i].name == row_name){
found = true;
new_datasets.splice(i,1);
break;
let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
array.splice(index, 1)
return true
}
}
return false
})
if(!found){
if (!element_found) {
new_datasets.push(entry);
}
let new_data = {
labels: raw_data.labels,
datasets: new_datasets
}
setTimeout(() => {
frappe.query_report.chart.update(new_data)
},500)
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
datasets: new_datasets,
};
chart_options = {
data: new_data,
type: "line",
};
frappe.query_report.render_chart(chart_options);
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))
# update last purchsae rate
if last_purchase_rate:
frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
(flt(last_purchase_rate), d.item_code))
frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
def validate_for_items(doc):
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

@@ -122,6 +122,12 @@ class AccountsController(TransactionBase):
def before_cancel(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):
for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
@@ -210,7 +216,10 @@ class AccountsController(TransactionBase):
self.meta.get_label(date_field), self)
def validate_inter_company_reference(self):
if self.is_internal_transfer() and self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
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. ")
@@ -293,6 +302,7 @@ class AccountsController(TransactionBase):
args["doctype"] = self.doctype
args["name"] = self.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"):
args["transaction_date"] = args.get("posting_date")
@@ -459,8 +469,10 @@ class AccountsController(TransactionBase):
account_currency = get_account_currency(gl_dict.account)
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)
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"),
self.company_currency)
@@ -1494,6 +1506,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.flags.ignore_validate_update_after_submit = True
parent.set_qty_as_per_stock_uom()
parent.calculate_taxes_and_totals()
parent.set_total_in_words()
if parent_doctype == "Sales Order":
make_packing_list(parent)
parent.set_gross_profit()

View File

@@ -372,7 +372,7 @@ class BuyingController(StockController):
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
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', '')
transferred_qty = raw_material.qty

View File

@@ -655,6 +655,34 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, 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.validate_and_sanitize_search_inputs
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.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice':
doc.consolidated_invoice = ""
doc.set('payments', [])
for data in source.payments:
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.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note":
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.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return

View File

@@ -232,7 +232,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate
'incoming_rate': p.get("incoming_rate")
}))
else:
il.append(frappe._dict({
@@ -251,7 +251,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate
'incoming_rate': d.get("incoming_rate")
}))
return il
@@ -456,9 +456,13 @@ class SellingController(StockController):
check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return
if self.doctype == "Sales Invoice" and self.is_consolidated:
return
if self.doctype == "POS Invoice":
return
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 '']
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note":
@@ -469,13 +473,19 @@ class SellingController(StockController):
non_stock_items = [d.item_code, d.description]
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:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
frappe.throw(duplicate_items_msg)
else:
check_list.append(stock_items)
else:
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:
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"],
["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
["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 import _
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.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController
@@ -23,7 +24,9 @@ class StockController(AccountsController):
self.validate_inspection()
self.validate_serialized_batch()
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):
if self.docstatus == 2:
@@ -71,7 +74,7 @@ class StockController(AccountsController):
gl_list = []
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:
sle_list = sle_map.get(item_row.name)
@@ -128,7 +131,13 @@ class StockController(AccountsController):
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))
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):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
@@ -241,7 +250,7 @@ class StockController(AccountsController):
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
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"
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")
@@ -393,6 +402,11 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
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():
@@ -419,6 +433,58 @@ class StockController(AccountsController):
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):
args = frappe._dict({
"posting_date": self.posting_date,
@@ -427,7 +493,6 @@ class StockController(AccountsController):
"voucher_no": self.name,
"company": self.company
})
if check_if_future_sle_exists(args):
create_repost_item_valuation_entry(args)
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
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.journal_entry.journal_entry import get_exchange_rate
class calculate_taxes_and_totals(object):
def __init__(self, doc):
@@ -108,7 +109,7 @@ class calculate_taxes_and_totals(object):
elif item.discount_amount and item.pricing_rules:
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)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
@@ -625,7 +626,6 @@ class calculate_taxes_and_totals(object):
self.doc.precision("base_write_off_amount"))
def calculate_margin(self, item):
rate_with_margin = 0.0
base_rate_with_margin = 0.0
if item.price_list_rate:
@@ -634,8 +634,8 @@ class calculate_taxes_and_totals(object):
for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
if (pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == self.doc.currency)\
or (pricing_rule.margin_type == 'Percentage'):
if pricing_rule.margin_rate_or_amount and ((pricing_rule.currency == self.doc.currency and
pricing_rule.margin_type in ['Amount', 'Percentage']) or pricing_rule.margin_type == 'Percentage'):
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
has_margin = True
@@ -777,3 +777,35 @@ def get_rounded_tax_amount(itemised_tax, precision):
for taxes in itemised_tax.values():
for tax_account in taxes:
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.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
@@ -56,6 +57,8 @@ def make_quality_inspection_template():
qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template
create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', {
"specification": "Moisture",
"value": "&lt; 5%",

View File

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

View File

@@ -176,7 +176,7 @@ class Lead(SellingController):
"phone": self.mobile_no
})
contact.insert()
contact.insert(ignore_permissions=True)
return contact

View File

@@ -8,12 +8,12 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-14 17:38:27.496696",
"modified": "2021-01-21 15:28:52.483839",
"modified_by": "Administrator",
"name": "Create Opportunity",
"owner": "Administrator",
"reference_document": "Opportunity",
"show_full_form": 0,
"show_full_form": 1,
"title": "Create Opportunity",
"validate_action": 1
}

View File

@@ -14,15 +14,15 @@ frappe.ui.form.on("Fees", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload: function(frm){
frm.set_query("academic_term",function(){
onload: function(frm) {
frm.set_query("academic_term", function() {
return{
"filters":{
"filters": {
"academic_year": (frm.doc.academic_year)
}
};
});
frm.set_query("fee_structure",function(){
frm.set_query("fee_structure", function() {
return{
"filters":{
"academic_year": (frm.doc.academic_year)

View File

@@ -124,21 +124,24 @@ class ProgramEnrollment(Document):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
if filters.get('program'):
return frappe.db.sql("""select course, course_name from `tabProgram Course`
where parent = %(program)s and course like %(txt)s {match_cond}
order by
if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
idx desc,
`tabProgram Course`.course asc
limit {start}, {page_len}""".format(
match_cond=get_match_cond(doctype),
start=start,
page_len=page_len), {
"txt": "%{0}%".format(txt),
"_txt": txt.replace('%', ''),
"program": filters['program']
})
if not filters.get('program'):
frappe.msgprint(_("Please select a Program first."))
return []
return frappe.db.sql("""select course, course_name from `tabProgram Course`
where parent = %(program)s and course like %(txt)s {match_cond}
order by
if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
idx desc,
`tabProgram Course`.course asc
limit {start}, {page_len}""".format(
match_cond=get_match_cond(doctype),
start=start,
page_len=page_len), {
"txt": "%{0}%".format(txt),
"_txt": txt.replace('%', ''),
"program": filters['program']
})
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@@ -5,7 +5,7 @@ import datetime
class MpesaConnector():
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
live_url="https://safaricom.co.ke"):
live_url="https://api.safaricom.co.ke"):
"""Setup configuration for Mpesa connector and generate new access token."""
self.env = env
self.app_key = app_key
@@ -102,14 +102,14 @@ class MpesaConnector():
"BusinessShortCode": business_shortcode,
"Password": encoded.decode("utf-8"),
"Timestamp": time,
"TransactionType": "CustomerPayBillOnline",
"Amount": amount,
"PartyA": int(phone_number),
"PartyB": business_shortcode,
"PartyB": reference_code,
"PhoneNumber": int(phone_number),
"CallBackURL": callback_url,
"AccountReference": reference_code,
"TransactionDesc": description
"TransactionDesc": description,
"TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline"
}
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}

View File

@@ -11,8 +11,10 @@
"consumer_secret",
"initiator_name",
"till_number",
"transaction_limit",
"sandbox",
"column_break_4",
"business_shortcode",
"online_passkey",
"security_credential",
"get_account_balance",
@@ -84,10 +86,24 @@
"fieldname": "get_account_balance",
"fieldtype": "Button",
"label": "Get Account Balance"
},
{
"depends_on": "eval:(doc.sandbox==0)",
"fieldname": "business_shortcode",
"fieldtype": "Data",
"label": "Business Shortcode",
"mandatory_depends_on": "eval:(doc.sandbox==0)"
},
{
"default": "150000",
"fieldname": "transaction_limit",
"fieldtype": "Float",
"label": "Transaction Limit",
"non_negative": 1
}
],
"links": [],
"modified": "2020-09-25 20:21:38.215494",
"modified": "2021-01-29 12:02:16.106942",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Mpesa Settings",

View File

@@ -33,13 +33,34 @@ class MpesaSettings(Document):
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
def request_for_payment(self, **kwargs):
if frappe.flags.in_test:
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
response = frappe._dict(get_payment_request_response_payload())
else:
response = frappe._dict(generate_stk_push(**kwargs))
args = frappe._dict(kwargs)
request_amounts = self.split_request_amount_according_to_transaction_limit(args)
self.handle_api_response("CheckoutRequestID", kwargs, response)
for i, amount in enumerate(request_amounts):
args.request_amount = amount
if frappe.flags.in_test:
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
response = frappe._dict(get_payment_request_response_payload(amount))
else:
response = frappe._dict(generate_stk_push(**args))
self.handle_api_response("CheckoutRequestID", args, response)
def split_request_amount_according_to_transaction_limit(self, args):
request_amount = args.request_amount
if request_amount > self.transaction_limit:
# make multiple requests
request_amounts = []
requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4
for i in range(requests_to_be_made):
amount = self.transaction_limit
if i == requests_to_be_made - 1:
amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30
request_amounts.append(amount)
else:
request_amounts = [request_amount]
return request_amounts
def get_account_balance_info(self):
payload = dict(
@@ -67,7 +88,8 @@ class MpesaSettings(Document):
req_name = getattr(response, global_id)
error = None
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
if not frappe.db.exists('Integration Request', req_name):
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
if error:
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
@@ -80,6 +102,8 @@ def generate_stk_push(**kwargs):
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
env = "production" if not mpesa_settings.sandbox else "sandbox"
# for sandbox, business shortcode is same as till number
business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
connector = MpesaConnector(env=env,
app_key=mpesa_settings.consumer_key,
@@ -87,10 +111,12 @@ def generate_stk_push(**kwargs):
mobile_number = sanitize_mobile_number(args.sender)
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
response = connector.stk_push(
business_shortcode=business_shortcode, amount=args.request_amount,
passcode=mpesa_settings.get_password("online_passkey"),
callback_url=callback_url, reference_code=mpesa_settings.till_number,
phone_number=mobile_number, description="POS Payment")
phone_number=mobile_number, description="POS Payment"
)
return response
@@ -108,29 +134,72 @@ def verify_transaction(**kwargs):
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(request.data))
integration_request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(integration_request.data))
total_paid = 0 # for multiple integration request made against a pos invoice
success = False # for reporting successfull callback to point of sale ui
if transaction_response['ResultCode'] == 0:
if request.reference_doctype and request.reference_docname:
if integration_request.reference_doctype and integration_request.reference_docname:
try:
doc = frappe.get_doc(request.reference_doctype,
request.reference_docname)
doc.run_method("on_payment_authorized", 'Completed')
item_response = transaction_response["CallbackMetadata"]["Item"]
amount = fetch_param_value(item_response, "Amount", "Name")
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt)
request.handle_success(transaction_response)
pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname)
mpesa_receipts, completed_payments = get_completed_integration_requests_info(
integration_request.reference_doctype,
integration_request.reference_docname,
checkout_id
)
total_paid = amount + sum(completed_payments)
mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt])
if total_paid >= pr.grand_total:
pr.run_method("on_payment_authorized", 'Completed')
success = True
frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
integration_request.handle_success(transaction_response)
except Exception:
request.handle_failure(transaction_response)
integration_request.handle_failure(transaction_response)
frappe.log_error(frappe.get_traceback())
else:
request.handle_failure(transaction_response)
integration_request.handle_failure(transaction_response)
frappe.publish_realtime('process_phone_payment', doctype="POS Invoice",
docname=transaction_data.payment_reference, user=request.owner, message=transaction_response)
frappe.publish_realtime(
event='process_phone_payment',
doctype="POS Invoice",
docname=transaction_data.payment_reference,
user=integration_request.owner,
message={
'amount': total_paid,
'success': success,
'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else ''
},
)
def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
output_of_other_completed_requests = frappe.get_all("Integration Request", filters={
'name': ['!=', checkout_id],
'reference_doctype': reference_doctype,
'reference_docname': reference_docname,
'status': 'Completed'
}, pluck="output")
mpesa_receipts, completed_payments = [], []
for out in output_of_other_completed_requests:
out = frappe._dict(loads(out))
item_response = out["CallbackMetadata"]["Item"]
completed_amount = fetch_param_value(item_response, "Amount", "Name")
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
completed_payments.append(completed_amount)
mpesa_receipts.append(completed_mpesa_receipt)
return mpesa_receipts, completed_payments
def get_account_balance(request_payload):
"""Call account balance API to send the request to the Mpesa Servers."""

View File

@@ -9,6 +9,10 @@ from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import p
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
class TestMpesaSettings(unittest.TestCase):
def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self):
create_mpesa_settings(payment_gateway_name="_Test")
@@ -40,10 +44,13 @@ class TestMpesaSettings(unittest.TestCase):
}
}))
integration_request.delete()
def test_processing_of_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500})
@@ -55,10 +62,16 @@ class TestMpesaSettings(unittest.TestCase):
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
callback_response = get_payment_callback_payload()
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0])
verify_transaction(**callback_response)
# test creation of integration request
integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972")
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
# test integration request creation and successful update of the status on receiving callback response
self.assertTrue(integration_request)
@@ -68,6 +81,122 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
self.assertEquals(integration_request.status, "Completed")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
integration_request.delete()
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
pos_invoice.contact_mobile = "093456543894"
pos_invoice.currency = "KES"
pos_invoice.save()
pr = pos_invoice.create_payment_request()
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
# create random receipt nos and send it as response to callback handler
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
integration_requests = []
for i in range(len(integration_req_ids)):
callback_response = get_payment_callback_payload(
Amount=500,
CheckoutRequestID=integration_req_ids[i],
MpesaReceiptNumber=mpesa_receipt_numbers[i]
)
# handle response manually
verify_transaction(**callback_response)
# test completion of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
self.assertEquals(integration_request.status, "Completed")
integration_requests.append(integration_request)
# check receipt number once all the integration requests are completed
pos_invoice.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers))
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
[d.delete() for d in integration_requests]
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
pos_invoice.contact_mobile = "093456543894"
pos_invoice.currency = "KES"
pos_invoice.save()
pr = pos_invoice.create_payment_request()
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
# create random receipt nos and send it as response to callback handler
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
callback_response = get_payment_callback_payload(
Amount=500,
CheckoutRequestID=integration_req_ids[0],
MpesaReceiptNumber=mpesa_receipt_numbers[0]
)
# handle response manually
verify_transaction(**callback_response)
# test completion of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
self.assertEquals(integration_request.status, "Completed")
# now one request is completed
# second integration request fails
# now retrying payment request should make only one integration request again
pr = pos_invoice.create_payment_request()
new_integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
'name': ['not in', integration_req_ids]
}, pluck="name")
self.assertEquals(len(new_integration_req_ids), 1)
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
def create_mpesa_settings(payment_gateway_name="Express"):
if frappe.db.exists("Mpesa Settings", payment_gateway_name):
@@ -157,16 +286,19 @@ def get_test_account_balance_response():
}
}
def get_payment_request_response_payload():
def get_payment_request_response_payload(Amount=500):
"""Response received after successfully calling the stk push process request API."""
CheckoutRequestID = frappe.utils.random_string(10)
return {
"MerchantRequestID": "8071-27184008-1",
"CheckoutRequestID": "ws_CO_061020201133231972",
"CheckoutRequestID": CheckoutRequestID,
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 500.0 },
{ "Name": "Amount", "Value": Amount },
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
{ "Name": "TransactionDate", "Value": 20201006113336 },
{ "Name": "PhoneNumber", "Value": 254723575670 }
@@ -174,41 +306,26 @@ def get_payment_request_response_payload():
}
}
def get_payment_callback_payload():
def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"):
"""Response received from the server as callback after calling the stkpush process request API."""
return {
"Body":{
"stkCallback":{
"MerchantRequestID":"19465-780693-1",
"CheckoutRequestID":"ws_CO_061020201133231972",
"ResultCode":0,
"ResultDesc":"The service request is processed successfully.",
"CallbackMetadata":{
"Item":[
{
"Name":"Amount",
"Value":500
},
{
"Name":"MpesaReceiptNumber",
"Value":"LGR7OWQX0R"
},
{
"Name":"Balance"
},
{
"Name":"TransactionDate",
"Value":20170727154800
},
{
"Name":"PhoneNumber",
"Value":254721566839
"stkCallback":{
"MerchantRequestID":"19465-780693-1",
"CheckoutRequestID":CheckoutRequestID,
"ResultCode":0,
"ResultDesc":"The service request is processed successfully.",
"CallbackMetadata":{
"Item":[
{ "Name":"Amount", "Value":Amount },
{ "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber },
{ "Name":"Balance" },
{ "Name":"TransactionDate", "Value":20170727154800 },
{ "Name":"PhoneNumber", "Value":254721566839 }
]
}
]
}
}
}
}
def get_account_balance_callback_payload():

View File

@@ -29,14 +29,11 @@ class PlaidConnector():
response = self.client.Item.public_token.exchange(public_token)
access_token = response["access_token"]
return access_token
def get_link_token(self):
def get_token_request(self, update_mode=False):
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
token_request = {
args = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": country_codes,
@@ -45,6 +42,20 @@ class PlaidConnector():
}
}
if update_mode:
args["access_token"] = self.access_token
else:
args.update({
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
})
return args
def get_link_token(self, update_mode=False):
token_request = self.get_token_request(update_mode)
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:

View File

@@ -12,9 +12,25 @@ frappe.ui.form.on('Plaid Settings', {
refresh: function (frm) {
if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => {
frm.add_custom_button(__('Link a new bank account'), () => {
new erpnext.integrations.plaidLink(frm);
});
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
freeze: true,
callback: () => {
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
frappe.msgprint({
title: __("Sync Started"),
message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]),
alert: 1
});
}
});
}).addClass("btn-primary");
}
}
});
@@ -30,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink {
this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename;
this.token = await this.frm.call("get_link_token").then(resp => resp.message);
this.token = await this.get_link_token();
this.init_plaid();
}
async get_link_token() {
const token = await this.frm.call("get_link_token").then(resp => resp.message);
if (!token) {
frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
}
return token;
}
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
@@ -78,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink {
}
onScriptError(error) {
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(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) {
@@ -107,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink {
});
}, __("Select a company"), __("Continue"));
}
};
};

View File

@@ -166,7 +166,6 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id
else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None
@@ -228,13 +227,23 @@ def new_bank_transaction(transaction):
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
enqueue_synchronization()
for plaid_account in plaid_accounts:
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)
@frappe.whitelist()
def enqueue_synchronization():
plaid_accounts = frappe.get_all("Bank Account",
filters={"integration_id": ["!=", ""]},
fields=["name", "bank"])
for plaid_account in plaid_accounts:
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)
@frappe.whitelist()
def get_link_token_for_update(access_token):
plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True)

View File

@@ -2,4 +2,82 @@
// For license information, please see license.txt
frappe.ui.form.on('Appointment Type', {
refresh: function(frm) {
frm.set_query('price_list', function() {
return {
filters: {'selling': 1}
};
});
frm.set_query('medical_department', 'items', function(doc) {
let item_list = doc.items.map(({medical_department}) => medical_department);
return {
filters: [
['Medical Department', 'name', 'not in', item_list]
]
};
});
frm.set_query('op_consulting_charge_item', 'items', function() {
return {
filters: {
is_stock_item: 0
}
};
});
frm.set_query('inpatient_visit_charge_item', 'items', function() {
return {
filters: {
is_stock_item: 0
}
};
});
}
});
frappe.ui.form.on('Appointment Type Service Item', {
op_consulting_charge_item: function(frm, cdt, cdn) {
let d = locals[cdt][cdn];
if (frm.doc.price_list && d.op_consulting_charge_item) {
frappe.call({
'method': 'frappe.client.get_value',
args: {
'doctype': 'Item Price',
'filters': {
'item_code': d.op_consulting_charge_item,
'price_list': frm.doc.price_list
},
'fieldname': ['price_list_rate']
},
callback: function(data) {
if (data.message.price_list_rate) {
frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
}
}
});
}
},
inpatient_visit_charge_item: function(frm, cdt, cdn) {
let d = locals[cdt][cdn];
if (frm.doc.price_list && d.inpatient_visit_charge_item) {
frappe.call({
'method': 'frappe.client.get_value',
args: {
'doctype': 'Item Price',
'filters': {
'item_code': d.inpatient_visit_charge_item,
'price_list': frm.doc.price_list
},
'fieldname': ['price_list_rate']
},
callback: function (data) {
if (data.message.price_list_rate) {
frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
}
}
});
}
}
});

View File

@@ -12,7 +12,10 @@
"appointment_type",
"ip",
"default_duration",
"color"
"color",
"billing_section",
"price_list",
"items"
],
"fields": [
{
@@ -52,10 +55,27 @@
"label": "Color",
"no_copy": 1,
"report_hide": 1
},
{
"fieldname": "billing_section",
"fieldtype": "Section Break",
"label": "Billing"
},
{
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List"
},
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Appointment Type Service Items",
"options": "Appointment Type Service Item"
}
],
"links": [],
"modified": "2020-02-03 21:06:05.833050",
"modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type",

View File

@@ -4,6 +4,53 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
class AppointmentType(Document):
pass
def validate(self):
if self.items and self.price_list:
for item in self.items:
existing_op_item_price = frappe.db.exists('Item Price', {
'item_code': item.op_consulting_charge_item,
'price_list': self.price_list
})
if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
existing_ip_item_price = frappe.db.exists('Item Price', {
'item_code': item.inpatient_visit_charge_item,
'price_list': self.price_list
})
if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
@frappe.whitelist()
def get_service_item_based_on_department(appointment_type, department):
item_list = frappe.db.get_value('Appointment Type Service Item',
filters = {'medical_department': department, 'parent': appointment_type},
fieldname = ['op_consulting_charge_item',
'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
as_dict = 1
)
# if department wise items are not set up
# use the generic items
if not item_list:
item_list = frappe.db.get_value('Appointment Type Service Item',
filters = {'parent': appointment_type},
fieldname = ['op_consulting_charge_item',
'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
as_dict = 1
)
return item_list
def make_item_price(price_list, item, item_price):
frappe.get_doc({
'doctype': 'Item Price',
'price_list': price_list,
'item_code': item,
'price_list_rate': item_price
}).insert(ignore_permissions=True, ignore_mandatory=True)

View File

@@ -0,0 +1,67 @@
{
"actions": [],
"creation": "2021-01-22 09:34:53.373105",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"medical_department",
"op_consulting_charge_item",
"op_consulting_charge",
"column_break_4",
"inpatient_visit_charge_item",
"inpatient_visit_charge"
],
"fields": [
{
"fieldname": "medical_department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Medical Department",
"options": "Medical Department"
},
{
"fieldname": "op_consulting_charge_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Out Patient Consulting Charge Item",
"options": "Item"
},
{
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Out Patient Consulting Charge"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "inpatient_visit_charge_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Inpatient Visit Charge Item",
"options": "Item"
},
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Inpatient Visit Charge Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-22 09:35:26.503443",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type Service Item",
"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) 2021, 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 AppointmentTypeServiceItem(Document):
pass

View File

@@ -100,7 +100,6 @@ class ClinicalProcedure(Document):
allow_start = self.set_actual_qty()
if allow_start:
self.db_set('status', 'In Progress')
insert_clinical_procedure_to_medical_record(self)
return 'success'
return 'insufficient stock'
@@ -122,6 +121,7 @@ class ClinicalProcedure(Document):
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.to_warehouse = self.warehouse
stock_entry.company = self.company
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for item in self.items:
if item.qty > item.actual_qty:
@@ -247,21 +247,3 @@ def make_procedure(source_name, target_doc=None):
}, target_doc, set_missing_values)
return doc
def insert_clinical_procedure_to_medical_record(doc):
subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "<br>"
if doc.practitioner:
subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
if subject and doc.notes:
subject += '<br/>' + doc.notes
medical_record = frappe.new_doc('Patient Medical Record')
medical_record.patient = doc.patient
medical_record.subject = subject
medical_record.status = 'Open'
medical_record.communication_date = doc.start_date
medical_record.reference_doctype = 'Clinical Procedure'
medical_record.reference_name = doc.name
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Copyright (c) 2017, ESS LLP and Contributors
# See license.txt
from __future__ import unicode_literals
@@ -60,6 +60,7 @@ def create_procedure(procedure_template, patient, practitioner):
procedure.practitioner = practitioner
procedure.consume_stock = procedure_template.allow_stock_consumption
procedure.items = procedure_template.items
procedure.warehouse = frappe.db.get_single_value('Stock Settings', 'default_warehouse')
procedure.company = "_Test Company"
procedure.warehouse = "_Test Warehouse - _TC"
procedure.submit()
return procedure

View File

@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"label": "Out Patient Consulting Charge",
"mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency"
},
{
@@ -174,7 +175,8 @@
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
"label": "Inpatient Visit Charge"
"label": "Inpatient Visit Charge",
"mandatory_depends_on": "inpatient_visit_charge_item"
},
{
"depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
],
"image_field": "image",
"links": [],
"modified": "2020-04-06 13:44:24.759623",
"modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",

View File

@@ -17,6 +17,9 @@
"enable_free_follow_ups",
"max_visits",
"valid_days",
"inpatient_settings_section",
"allow_discharge_despite_unbilled_services",
"do_not_bill_inpatient_encounters",
"healthcare_service_items",
"inpatient_visit_charge_item",
"op_consulting_charge_item",
@@ -302,11 +305,28 @@
"fieldname": "enable_free_follow_ups",
"fieldtype": "Check",
"label": "Enable Free Follow-ups"
},
{
"fieldname": "inpatient_settings_section",
"fieldtype": "Section Break",
"label": "Inpatient Settings"
},
{
"default": "0",
"fieldname": "allow_discharge_despite_unbilled_services",
"fieldtype": "Check",
"label": "Allow Discharge Despite Unbilled Healthcare Services"
},
{
"default": "0",
"fieldname": "do_not_bill_inpatient_encounters",
"fieldtype": "Check",
"label": "Do Not Bill Patient Encounters for Inpatients"
}
],
"issingle": 1,
"links": [],
"modified": "2020-07-08 15:17:21.543218",
"modified": "2021-01-13 09:04:35.877700",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Settings",

View File

@@ -5,6 +5,7 @@ frappe.ui.form.on('Inpatient Medication Entry', {
refresh: function(frm) {
// Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.set_query('item_code', () => {
return {

View File

@@ -139,7 +139,6 @@
"fieldtype": "Table",
"label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail",
"read_only": 1,
"reqd": 1
},
{
@@ -180,7 +179,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-03 13:22:37.820707",
"modified": "2021-01-11 12:37:46.749659",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",

View File

@@ -15,8 +15,6 @@ class InpatientMedicationEntry(Document):
self.validate_medication_orders()
def get_medication_orders(self):
self.validate_datetime_filters()
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
@@ -27,22 +25,6 @@ class InpatientMedicationEntry(Document):
self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria'))
def validate_datetime_filters(self):
if self.from_date and self.to_date:
self.validate_from_to_dates('from_date', 'to_date')
if self.from_date and getdate(self.from_date) > getdate():
frappe.throw(_('From Date cannot be after the current date.'))
if self.to_date and getdate(self.to_date) > getdate():
frappe.throw(_('To Date cannot be after the current date.'))
if self.from_time and self.from_time > nowtime():
frappe.throw(_('From Time cannot be after the current time.'))
if self.to_time and self.to_time > nowtime():
frappe.throw(_('To Time cannot be after the current time.'))
def add_mo_to_table(self, orders):
# Add medication orders in the child table
self.set('medication_orders', [])
@@ -282,7 +264,7 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
if ip_record.inpatient_occupancies:
if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit
return

View File

@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
from frappe.utils import today, now_datetime, getdate, get_datetime
from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond
@@ -113,6 +113,7 @@ def schedule_inpatient(args):
inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True)
@frappe.whitelist()
def schedule_discharge(args):
discharge_order = json.loads(args)
@@ -126,16 +127,19 @@ def schedule_discharge(args):
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order:
inpatient_record.set(key, ip_order[key])
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child:
table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname))
def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record):
inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
def discharge_patient(inpatient_record):
validate_invoiced_inpatient(inpatient_record)
validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True)
def validate_invoiced_inpatient(inpatient_record):
pending_invoices = []
def validate_inpatient_invoicing(inpatient_record):
if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
return
pending_invoices = get_pending_invoices(inpatient_record)
if pending_invoices:
message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
formatted_doc_rows = ''
for doctype, docnames in pending_invoices.items():
formatted_doc_rows += """
<td>{0}</td>
<td>{1}</td>
</tr>""".format(doctype, docnames)
message += """
<table class='table'>
<thead>
<th>{0}</th>
<th>{1}</th>
</thead>
{2}
</table>
""".format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
def get_pending_invoices(inpatient_record):
pending_invoices = {}
if inpatient_record.inpatient_occupancies:
service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
if inpatient_occupancy.invoiced != 1:
if not inpatient_occupancy.invoiced:
if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit
else:
service_unit_names = inpatient_occupancy.service_unit
if service_unit_names:
pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")")
pending_invoices["Inpatient Occupancy"] = service_unit_names
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs:
doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record)
doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
if pending_invoices:
frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
.join(pending_invoices)), title=_('Unbilled Invoices'))
return pending_invoices
def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list:
doc_ids = False
for doc_name in doc_name_list:
doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids:
doc_ids += ", "+doc_name.name
doc_ids += ", " + doc_link
else:
doc_ids = doc_name.name
doc_ids = doc_link
if doc_ids:
pending_invoices.append(doc + " (" + doc_ids + ")")
pending_invoices[doc] = doc_ids
return pending_invoices
def get_inpatient_docs_not_invoiced(doc, inpatient_record):
def get_unbilled_inpatient_docs(doc, inpatient_record):
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted'
@@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit
@@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in):
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from):
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters):

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