Merge branch 'develop' into escape_sql_credit_report

This commit is contained in:
Ankush Menat 2021-04-12 14:37:31 +05:30 committed by GitHub
commit 53ce877ad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
209 changed files with 2834 additions and 1691 deletions

View File

@ -214,6 +214,7 @@ class Account(NestedSet):
if parent_value_changed: if parent_value_changed:
doc.save() doc.save()
@frappe.whitelist()
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
if self.check_if_child_exists(): if self.check_if_child_exists():
throw(_("Account with child nodes cannot be converted to ledger")) throw(_("Account with child nodes cannot be converted to ledger"))
@ -224,6 +225,7 @@ class Account(NestedSet):
self.save() self.save()
return 1 return 1
@frappe.whitelist()
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if self.check_gle_exists(): if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group.")) throw(_("Account with existing transaction can not be converted to group."))

View File

@ -39,6 +39,7 @@ class AccountingPeriod(Document):
frappe.throw(_("Accounting Period overlaps with {0}") frappe.throw(_("Accounting Period overlaps with {0}")
.format(existing_accounting_period[0].get("name")), OverlapError) .format(existing_accounting_period[0].get("name")), OverlapError)
@frappe.whitelist()
def get_doctypes_for_closing(self): def get_doctypes_for_closing(self):
docs_for_closing = [] docs_for_closing = []
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \ doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \

View File

@ -12,6 +12,7 @@ form_grid_templates = {
} }
class BankClearance(Document): class BankClearance(Document):
@frappe.whitelist()
def get_payment_entries(self): def get_payment_entries(self):
if not (self.from_date and self.to_date): if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory")) frappe.throw(_("From Date and To Date are Mandatory"))
@ -108,6 +109,7 @@ class BankClearance(Document):
row.update(d) row.update(d)
self.total_amount += flt(amount) self.total_amount += flt(amount)
@frappe.whitelist()
def update_clearance_date(self): def update_clearance_date(self):
clearance_date_updated = False clearance_date_updated = False
for d in self.get('payment_entries'): for d in self.get('payment_entries'):

View File

@ -532,43 +532,4 @@ frappe.ui.form.on("Bank Statement Import", {
</table> </table>
`); `);
}, },
show_missing_link_values(frm, missing_link_values) {
let can_be_created_automatically = missing_link_values.every(
(d) => d.has_one_mandatory_field
);
let html = missing_link_values
.map((d) => {
let doctype = d.doctype;
let values = d.missing_values;
return `
<h5>${doctype}</h5>
<ul>${values.map((v) => `<li>${v}</li>`).join("")}</ul>
`;
})
.join("");
if (can_be_created_automatically) {
// prettier-ignore
let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?');
frappe.confirm(message + html, () => {
frm.call("create_missing_link_values", {
missing_link_values,
}).then((r) => {
let records = r.message;
frappe.msgprint(__(
"Created {0} records successfully.", [
records.length,
]
));
});
});
} else {
frappe.msgprint(
// prettier-ignore
__('The following records needs to be created before we can import your file.') + html
);
}
},
}); });

View File

@ -15,12 +15,14 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
test_dependencies = ["Item", "Cost Center"] test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase): class TestBankTransaction(unittest.TestCase):
def setUp(self): @classmethod
def setUpClass(cls):
make_pos_profile() make_pos_profile()
add_transactions() add_transactions()
add_vouchers() add_vouchers()
def tearDown(self): @classmethod
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"): for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name) doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel() doc.cancel()
@ -33,9 +35,6 @@ class TestBankTransaction(unittest.TestCase):
# Delete POS Profile # Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self): def test_linked_payments(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
@ -44,8 +43,8 @@ class TestBankTransaction(unittest.TestCase):
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self): def test_reconcile(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps([{ vouchers = json.dumps([{
"payment_doctype":"Payment Entry", "payment_doctype":"Payment Entry",
"payment_name":payment.name, "payment_name":payment.name,
@ -62,7 +61,6 @@ class TestBankTransaction(unittest.TestCase):
def test_debit_credit_output(self): def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
print(linked_payments)
self.assertTrue(linked_payments[0][3]) self.assertTrue(linked_payments[0][3])
# Check error if already reconciled # Check error if already reconciled
@ -116,10 +114,6 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
pass pass
def add_transactions(): def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
create_bank_account() create_bank_account()
doc = frappe.get_doc({ doc = frappe.get_doc({
@ -172,14 +166,8 @@ def add_transactions():
}).insert() }).insert()
doc.submit() doc.submit()
frappe.flags.test_bank_transactions_created = True
def add_vouchers(): def add_vouchers():
if frappe.flags.test_payments_created:
return
frappe.set_user("Administrator")
try: try:
frappe.get_doc({ frappe.get_doc({
"doctype": "Supplier", "doctype": "Supplier",
@ -272,13 +260,6 @@ def add_vouchers():
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Fayva Oct 18"
pe.reference_date = "2018-10-29"
pe.insert()
pe.submit()
mode_of_payment = frappe.get_doc({ mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment", "doctype": "Mode of Payment",
"name": "Cash" "name": "Cash"
@ -291,14 +272,12 @@ def add_vouchers():
}) })
mode_of_payment.save() mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1) si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1 si.is_pos = 1
si.append("payments", { si.append("payments", {
"mode_of_payment": "Cash", "mode_of_payment": "Cash",
"account": "_Test Bank - _TC", "account": "_Test Bank - _TC",
"amount": 109080 "amount": 109080
}) })
si.save() si.insert()
si.submit() si.submit()
frappe.flags.test_payments_created = True

View File

@ -57,6 +57,7 @@ class CForm(Document):
total = sum([flt(d.grand_total) for d in self.get('invoices')]) total = sum([flt(d.grand_total) for d in self.get('invoices')])
frappe.db.set(self, 'total_invoiced_amount', total) frappe.db.set(self, 'total_invoiced_amount', total)
@frappe.whitelist()
def get_invoice_details(self, invoice_no): def get_invoice_details(self, invoice_no):
""" Pull details from invoices for referrence """ """ Pull details from invoices for referrence """
if invoice_no: if invoice_no:

View File

@ -293,6 +293,11 @@ def validate_accounts(file_name):
accounts_dict = {} accounts_dict = {}
for account in accounts: for account in accounts:
accounts_dict.setdefault(account["account_name"], account) accounts_dict.setdefault(account["account_name"], account)
if not hasattr(account, "parent_account"):
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
msg += "<br><br>"
msg += _("Alternatively, you can download the template and fill your data in.")
frappe.throw(msg, title=_("Parent Account Missing"))
if account["parent_account"] and accounts_dict.get(account["parent_account"]): if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1 accounts_dict[account["parent_account"]]["is_group"] = 1

View File

@ -50,6 +50,7 @@ class CostCenter(NestedSet):
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format( frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
frappe.bold(self.parent_cost_center))) frappe.bold(self.parent_cost_center)))
@frappe.whitelist()
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
if self.check_if_child_exists(): if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes")) frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
@ -60,6 +61,7 @@ class CostCenter(NestedSet):
self.save() self.save()
return 1 return 1
@frappe.whitelist()
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center): if cint(self.enable_distributed_cost_center):
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))

View File

@ -27,6 +27,7 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date): if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
@frappe.whitelist()
def get_accounts_data(self, account=None): def get_accounts_data(self, account=None):
accounts = [] accounts = []
self.validate_mandatory() self.validate_mandatory()
@ -95,6 +96,7 @@ class ExchangeRateRevaluation(Document):
message = _("No outstanding invoices found") message = _("No outstanding invoices found")
frappe.msgprint(message) frappe.msgprint(message)
@frappe.whitelist()
def make_jv_entry(self): def make_jv_entry(self):
if self.total_gain_loss == 0: if self.total_gain_loss == 0:
return return

View File

@ -12,6 +12,7 @@ from frappe.model.document import Document
class FiscalYearIncorrectDate(frappe.ValidationError): pass class FiscalYearIncorrectDate(frappe.ValidationError): pass
class FiscalYear(Document): class FiscalYear(Document):
@frappe.whitelist()
def set_as_default(self): def set_as_default(self):
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name) frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults") global_defaults = frappe.get_doc("Global Defaults")
@ -54,7 +55,7 @@ class FiscalYear(Document):
def on_update(self): def on_update(self):
check_duplicate_fiscal_year(self) check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years") frappe.cache().delete_value("fiscal_years")
def on_trash(self): def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults") global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name: if global_defaults.current_fiscal_year == self.name:

View File

@ -290,4 +290,8 @@ def rename_temporarily_named_docs(doctype):
oldname = doc.name oldname = doc.name
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc) set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name newname = doc.name
frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname)) frappe.db.sql(
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
(newname, oldname),
auto_commit=True
)

View File

@ -125,6 +125,7 @@ class InvoiceDiscounting(AccountsController):
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
@frappe.whitelist()
def create_disbursement_entry(self): def create_disbursement_entry(self):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry' je.voucher_type = 'Journal Entry'
@ -174,6 +175,7 @@ class InvoiceDiscounting(AccountsController):
return je return je
@frappe.whitelist()
def close_loan(self): def close_loan(self):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry' je.voucher_type = 'Journal Entry'

View File

@ -564,6 +564,7 @@ class JournalEntry(AccountsController):
if gl_map: if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self): def get_balance(self):
if not self.get('accounts'): if not self.get('accounts'):
msgprint(_("'Entries' cannot be empty"), raise_exception=True) msgprint(_("'Entries' cannot be empty"), raise_exception=True)

View File

@ -8,6 +8,7 @@ from frappe.utils import (flt, add_months)
from frappe.model.document import Document from frappe.model.document import Document
class MonthlyDistribution(Document): class MonthlyDistribution(Document):
@frappe.whitelist()
def get_months(self): def get_months(self):
month_list = ['January','February','March','April','May','June','July','August','September', month_list = ['January','February','March','April','May','June','July','August','September',
'October','November','December'] 'October','November','December']

View File

@ -167,6 +167,7 @@ class OpeningInvoiceCreationTool(Document):
return invoice return invoice
@frappe.whitelist()
def make_invoices(self): def make_invoices(self):
self.validate_company() self.validate_company()
invoices = self.get_invoices() invoices = self.get_invoices()

View File

@ -637,13 +637,13 @@ frappe.ui.form.on('Payment Entry', {
let to_field = fields[key][1]; let to_field = fields[key][1];
if (filters[from_field] && !filters[to_field]) { if (filters[from_field] && !filters[to_field]) {
frappe.throw(__("Error: {0} is mandatory field", frappe.throw(
[to_field.replace(/_/g, " ")] __("Error: {0} is mandatory field", [to_field.replace(/_/g, " ")])
)); );
} else if (filters[from_field] && filters[from_field] > filters[to_field]) { } else if (filters[from_field] && filters[from_field] > filters[to_field]) {
frappe.throw(__("{0}: {1} must be less than {2}", frappe.throw(
[key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")] __("{0}: {1} must be less than {2}", [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")])
)); );
} }
} }
}, },
@ -692,6 +692,8 @@ frappe.ui.form.on('Payment Entry', {
c.total_amount = d.invoice_amount; c.total_amount = d.invoice_amount;
c.outstanding_amount = d.outstanding_amount; c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no; c.bill_no = d.bill_no;
c.payment_term = d.payment_term;
c.allocated_amount = d.allocated_amount;
if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) { if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) {
if(flt(d.outstanding_amount) > 0) if(flt(d.outstanding_amount) > 0)
@ -774,12 +776,15 @@ frappe.ui.form.on('Payment Entry', {
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) { } else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
if(paid_amount > total_negative_outstanding) { if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) { if(total_negative_outstanding == 0) {
frappe.msgprint(__("Cannot {0} {1} {2} without any negative outstanding invoice", frappe.msgprint(
[frm.doc.payment_type, __("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type,
(frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])); (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])
);
return false return false
} else { } else {
frappe.msgprint(__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])); frappe.msgprint(
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])
);
return false; return false;
} }
} else { } else {
@ -791,10 +796,13 @@ frappe.ui.form.on('Payment Entry', {
} }
$.each(frm.doc.references || [], function(i, row) { $.each(frm.doc.references || [], function(i, row) {
row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount if (frappe.flags.allocate_payment_amount == 0) {
if(frappe.flags.allocate_payment_amount != 0){ //If allocate payment amount checkbox is unchecked, set zero to allocate amount
if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) { row.allocated_amount = 0;
if(row.outstanding_amount >= allocated_positive_outstanding) {
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding; row.allocated_amount = allocated_positive_outstanding;
} else { } else {
row.allocated_amount = row.outstanding_amount; row.allocated_amount = row.outstanding_amount;
@ -802,9 +810,11 @@ frappe.ui.form.on('Payment Entry', {
allocated_positive_outstanding -= flt(row.allocated_amount); allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
if(Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding; row.allocated_amount = -1*allocated_negative_outstanding;
else row.allocated_amount = row.outstanding_amount; } else {
row.allocated_amount = row.outstanding_amount;
};
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
} }

View File

@ -333,33 +333,50 @@ class PaymentEntry(AccountsController):
invoice_payment_amount_map = {} invoice_payment_amount_map = {}
invoice_paid_amount_map = {} invoice_paid_amount_map = {}
for reference in self.get('references'): for ref in self.get('references'):
if reference.payment_term and reference.reference_name: if ref.payment_term and ref.reference_name:
key = (reference.payment_term, reference.reference_name) key = (ref.payment_term, ref.reference_name)
invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += reference.allocated_amount invoice_payment_amount_map[key] += ref.allocated_amount
if not invoice_paid_amount_map.get(key): if not invoice_paid_amount_map.get(key):
payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, payment_schedule = frappe.get_all(
fields=['paid_amount', 'payment_amount', 'payment_term']) 'Payment Schedule',
filters={'parent': ref.reference_name},
fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding']
)
for term in payment_schedule: for term in payment_schedule:
invoice_key = (term.payment_term, reference.reference_name) invoice_key = (term.payment_term, ref.reference_name)
invoice_paid_amount_map.setdefault(invoice_key, {}) invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
for key, allocated_amount in iteritems(invoice_payment_amount_map):
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
for key, amount in iteritems(invoice_payment_amount_map):
if cancel: if cancel:
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s frappe.db.sql("""
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` - %s,
discounted_amount = `discounted_amount` - %s,
outstanding = `outstanding` + %s
WHERE parent = %s and payment_term = %s""",
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
else: else:
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding')) if allocated_amount > outstanding:
if amount > outstanding:
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
if amount and outstanding: if allocated_amount and outstanding:
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s frappe.db.sql("""
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` + %s,
discounted_amount = `discounted_amount` + %s,
outstanding = `outstanding` - %s
WHERE parent = %s and payment_term = %s""",
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
def set_status(self): def set_status(self):
if self.docstatus == 2: if self.docstatus == 2:
@ -708,6 +725,8 @@ def get_outstanding_reference_documents(args):
outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"), outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
args.get("party_account"), filters=args, condition=condition) args.get("party_account"), filters=args, condition=condition)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
for d in outstanding_invoices: for d in outstanding_invoices:
d["exchange_rate"] = 1 d["exchange_rate"] = 1
if party_account_currency != company_currency: if party_account_currency != company_currency:
@ -735,6 +754,46 @@ def get_outstanding_reference_documents(args):
return data return data
def split_invoices_based_on_payment_terms(outstanding_invoices):
invoice_ref_based_on_payment_terms = {}
for idx, d in enumerate(outstanding_invoices):
if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']:
payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template')
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.db.get_value(
'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms')
if allocate_payment_based_on_payment_terms:
payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"])
for payment_term in payment_schedule:
if payment_term.outstanding > 0.1:
invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append(frappe._dict({
'due_date': d.due_date,
'currency': d.currency,
'voucher_no': d.voucher_no,
'voucher_type': d.voucher_type,
'posting_date': d.posting_date,
'invoice_amount': flt(d.invoice_amount),
'outstanding_amount': flt(d.outstanding_amount),
'payment_amount': payment_term.payment_amount,
'payment_term': payment_term.payment_term,
'allocated_amount': payment_term.outstanding
}))
if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items():
voucher_no = outstanding_invoices[idx]['voucher_no']
voucher_type = outstanding_invoices[idx]['voucher_type']
frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
voucher_type, voucher_no, len(ref)), alert=True)
outstanding_invoices.pop(idx - 1)
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
return outstanding_invoices
def get_orders_to_be_billed(posting_date, party_type, party, def get_orders_to_be_billed(posting_date, party_type, party,
company, party_account_currency, company_currency, cost_center=None, filters=None): company, party_account_currency, company_currency, cost_center=None, filters=None):
if party_type == "Customer": if party_type == "Customer":
@ -1091,6 +1150,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
paid_amount, received_amount = set_paid_amount_and_received_amount( paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc) dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc)
pe = frappe.new_doc("Payment Entry") pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type pe.payment_type = payment_type
pe.company = doc.company pe.company = doc.company
@ -1160,11 +1221,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.setup_party_account_field() pe.setup_party_account_field()
pe.set_missing_values() pe.set_missing_values()
if party_account and bank: if party_account and bank:
if dt == "Employee Advance": if dt == "Employee Advance":
reference_doc = doc reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc) pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts() pe.set_amounts()
if discount_amount:
pe.set_gain_or_loss(account_details={
'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"),
'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"),
'amount': discount_amount * (-1 if payment_type == "Pay" else 1)
})
pe.set_difference_amount()
return pe return pe
def get_bank_cash_account(doc, bank_account): def get_bank_cash_account(doc, bank_account):
@ -1285,6 +1355,33 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
paid_amount = received_amount * doc.get('exchange_rate', 1) paid_amount = received_amount * doc.get('exchange_rate', 1)
return paid_amount, received_amount return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':
discount_amount = flt(doc.get('grand_total')) * (term.discount / 100)
else:
discount_amount = term.discount
discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1)
if doc.doctype == 'Sales Invoice':
paid_amount -= discount_amount
received_amount -= discount_amount_in_foreign_currency
else:
received_amount -= discount_amount
paid_amount -= discount_amount_in_foreign_currency
total_discount += discount_amount
if total_discount:
money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency'))
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
return paid_amount, received_amount, total_discount
def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
references = [] references = []
for payment_term in payment_schedule: for payment_term in payment_schedule:

View File

@ -193,6 +193,34 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()
si.payment_terms_template = 'Test Discount Template'
frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC')
si.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18
})
si.save()
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.submit()
si.load_from_db()
self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount')
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 212.40)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
def test_payment_against_purchase_invoice_to_check_status(self): def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
@ -591,6 +619,26 @@ def create_payment_terms_template():
}] }]
}).insert() }).insert()
def create_payment_terms_template_with_discount():
create_payment_term('30 Credit Days with 10% Discount')
if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'):
payment_term_template = frappe.get_doc({
'doctype': 'Payment Terms Template',
'template_name': 'Test Discount Template',
'allocate_payment_based_on_payment_terms': 1,
'terms': [{
'doctype': 'Payment Terms Template Detail',
'payment_term': '30 Credit Days with 10% Discount',
'invoice_portion': 100,
'credit_days_based_on': 'Day(s) after invoice date',
'credit_days': 2,
'discount': 10,
'discount_validity_based_on': 'Day(s) after invoice date',
'discount_validity': 1
}]
}).insert()
def create_payment_term(name): def create_payment_term(name):
if not frappe.db.exists('Payment Term', name): if not frappe.db.exists('Payment Term', name):

View File

@ -58,7 +58,7 @@
"fieldname": "total_amount", "fieldname": "total_amount",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Total Amount", "label": "Grand Total",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -92,9 +92,10 @@
"options": "Payment Term" "options": "Payment Term"
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-13 12:07:19.362539", "modified": "2021-02-10 11:25:47.144392",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry Reference", "name": "Payment Entry Reference",

View File

@ -11,6 +11,7 @@ from erpnext.accounts.utils import (get_outstanding_invoices,
from erpnext.controllers.accounts_controller import get_advance_payment_entries from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document): class PaymentReconciliation(Document):
@frappe.whitelist()
def get_unreconciled_entries(self): def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries() self.get_nonreconciled_payment_entries()
self.get_invoice_entries() self.get_invoice_entries()
@ -147,6 +148,7 @@ class PaymentReconciliation(Document):
ent.currency = e.get('currency') ent.currency = e.get('currency')
ent.outstanding_amount = e.get('outstanding_amount') ent.outstanding_amount = e.get('outstanding_amount')
@frappe.whitelist()
def reconcile(self, args): def reconcile(self, args):
for e in self.get('payments'): for e in self.get('payments'):
e.invoice_type = None e.invoice_type = None
@ -197,6 +199,7 @@ class PaymentReconciliation(Document):
'difference_account': row.difference_account 'difference_account': row.difference_account
}) })
@frappe.whitelist()
def get_difference_amount(self, child_row): def get_difference_amount(self, child_row):
if child_row.get("reference_type") != 'Payment Entry': return if child_row.get("reference_type") != 'Payment Entry': return

View File

@ -6,11 +6,23 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"payment_term", "payment_term",
"section_break_15",
"description", "description",
"section_break_4",
"due_date", "due_date",
"invoice_portion",
"payment_amount",
"mode_of_payment", "mode_of_payment",
"column_break_5",
"invoice_portion",
"section_break_6",
"discount_type",
"discount_date",
"column_break_9",
"discount",
"section_break_9",
"payment_amount",
"discounted_amount",
"column_break_3",
"outstanding",
"paid_amount" "paid_amount"
], ],
"fields": [ "fields": [
@ -25,6 +37,7 @@
}, },
{ {
"columns": 2, "columns": 2,
"fetch_from": "payment_term.description",
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"in_list_view": 1, "in_list_view": 1,
@ -62,14 +75,82 @@
"options": "Mode of Payment" "options": "Mode of Payment"
}, },
{ {
"depends_on": "paid_amount",
"fieldname": "paid_amount", "fieldname": "paid_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Amount" "label": "Paid Amount"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "discounted_amount",
"fieldname": "discounted_amount",
"fieldtype": "Currency",
"label": "Discounted Amount",
"read_only": 1
},
{
"fetch_from": "payment_amount",
"fieldname": "outstanding",
"fieldtype": "Currency",
"label": "Outstanding",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"depends_on": "discount",
"fieldname": "discount_date",
"fieldtype": "Date",
"label": "Discount Date",
"mandatory_depends_on": "discount"
},
{
"default": "Percentage",
"fetch_from": "payment_term.discount_type",
"fieldname": "discount_type",
"fieldtype": "Select",
"label": "Discount Type",
"options": "Percentage\nAmount"
},
{
"fetch_from": "payment_term.discount",
"fieldname": "discount",
"fieldtype": "Float",
"label": "Discount"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-13 17:58:24.729526", "modified": "2021-02-15 21:03:12.540546",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Schedule", "name": "Payment Schedule",

View File

@ -1,2 +1,22 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Payment Term', {
onload(frm) {
frm.trigger('set_dynamic_description');
},
discount(frm) {
frm.trigger('set_dynamic_description');
},
discount_type(frm) {
frm.trigger('set_dynamic_description');
},
set_dynamic_description(frm) {
if (frm.doc.discount) {
let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]);
if (frm.doc.discount_type == 'Amount') {
description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]);
}
frm.set_df_property("discount", "description", description);
}
}
});

View File

@ -1,386 +1,166 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "allow_import": 1,
"allow_import": 1, "allow_rename": 1,
"allow_rename": 1, "autoname": "field:payment_term_name",
"autoname": "field:payment_term_name", "creation": "2017-08-10 15:24:54.876365",
"beta": 0, "doctype": "DocType",
"creation": "2017-08-10 15:24:54.876365", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "payment_term_name",
"document_type": "", "invoice_portion",
"editable_grid": 1, "mode_of_payment",
"engine": "InnoDB", "column_break_3",
"due_date_based_on",
"credit_days",
"credit_months",
"section_break_8",
"discount_type",
"discount",
"column_break_11",
"discount_validity_based_on",
"discount_validity",
"section_break_6",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "fieldname": "payment_term_name",
"bold": 1, "fieldtype": "Data",
"collapsible": 0, "label": "Payment Term Name",
"columns": 0, "unique": 1
"fieldname": "payment_term_name", },
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payment Term Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"description": "Provide the invoice portion in percent", "bold": 1,
"allow_bulk_edit": 0, "fieldname": "invoice_portion",
"allow_on_submit": 0, "fieldtype": "Float",
"bold": 1, "label": "Invoice Portion (%)"
"collapsible": 0, },
"columns": 0,
"fieldname": "invoice_portion",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Invoice Portion",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "mode_of_payment",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Mode of Payment",
"collapsible": 0, "options": "Mode of Payment"
"columns": 0, },
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Mode of Payment",
"length": 0,
"no_copy": 0,
"options": "Mode of Payment",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "fieldname": "due_date_based_on",
"bold": 1, "fieldtype": "Select",
"collapsible": 0, "label": "Due Date Based On",
"columns": 0, "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
"fieldname": "due_date_based_on", },
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Due Date Based On",
"length": 0,
"no_copy": 0,
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"description": "Give number of days according to prior selection", "bold": 1,
"allow_bulk_edit": 0, "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"allow_on_submit": 0, "fieldname": "credit_days",
"bold": 1, "fieldtype": "Int",
"collapsible": 0, "label": "Credit Days"
"columns": 0, },
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Credit Days",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"allow_on_submit": 0, "fieldname": "credit_months",
"bold": 0, "fieldtype": "Int",
"collapsible": 0, "label": "Credit Months"
"columns": 0, },
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Credit Months",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_6",
"allow_on_submit": 0, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "fieldname": "description",
"bold": 1, "fieldtype": "Small Text",
"collapsible": 0, "label": "Description"
"columns": 0, },
"fieldname": "description", {
"fieldtype": "Small Text", "fieldname": "section_break_8",
"hidden": 0, "fieldtype": "Section Break",
"ignore_user_permissions": 0, "label": "Discount Settings"
"ignore_xss_filter": 0, },
"in_filter": 0, {
"in_global_search": 0, "default": "Percentage",
"in_list_view": 0, "fieldname": "discount_type",
"in_standard_filter": 0, "fieldtype": "Select",
"label": "Description", "label": "Discount Type",
"length": 0, "options": "Percentage\nAmount"
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "discount",
"print_hide": 0, "fieldtype": "Float",
"print_hide_if_no_value": 0, "label": "Discount"
"read_only": 0, },
"remember_last_selected_value": 0, {
"report_hide": 0, "default": "Day(s) after invoice date",
"reqd": 0, "depends_on": "discount",
"search_index": 0, "fieldname": "discount_validity_based_on",
"set_only_once": 0, "fieldtype": "Select",
"translatable": 0, "label": "Discount Validity Based On",
"unique": 0 "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
},
{
"depends_on": "discount",
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"mandatory_depends_on": "discount"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 0, "links": [],
"hide_heading": 0, "modified": "2021-02-15 20:30:56.256403",
"hide_toolbar": 0, "modified_by": "Administrator",
"idx": 0, "module": "Accounts",
"image_view": 0, "name": "Payment Term",
"in_create": 0, "owner": "Administrator",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-10-14 10:47:32.830478",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Term",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "System Manager",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "Accounts Manager",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "Accounts User",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC",
"show_name_in_global_search": 0, "track_changes": 1
"sort_field": "modified", }
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@ -3,11 +3,6 @@
frappe.ui.form.on('Payment Terms Template', { frappe.ui.form.on('Payment Terms Template', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("payment_term", "description", "description");
frm.add_fetch("payment_term", "invoice_portion", "invoice_portion");
frm.add_fetch("payment_term", "due_date_based_on", "due_date_based_on");
frm.add_fetch("payment_term", "credit_days", "credit_days");
frm.add_fetch("payment_term", "credit_months", "credit_months");
frm.add_fetch("payment_term", "mode_of_payment", "mode_of_payment");
} }
}); });

View File

@ -13,7 +13,6 @@ from frappe import _
class PaymentTermsTemplate(Document): class PaymentTermsTemplate(Document):
def validate(self): def validate(self):
self.validate_invoice_portion() self.validate_invoice_portion()
self.validate_credit_days()
self.check_duplicate_terms() self.check_duplicate_terms()
def validate_invoice_portion(self): def validate_invoice_portion(self):
@ -24,11 +23,6 @@ class PaymentTermsTemplate(Document):
if flt(total_portion, 2) != 100.00: if flt(total_portion, 2) != 100.00:
frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red') frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
def validate_credit_days(self):
for term in self.terms:
if cint(term.credit_days) < 0:
frappe.msgprint(_('Credit Days cannot be a negative number'), raise_exception=1, indicator='red')
def check_duplicate_terms(self): def check_duplicate_terms(self):
terms = [] terms = []
for term in self.terms: for term in self.terms:

View File

@ -1,278 +1,164 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2017-08-10 15:34:09.409562",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"autoname": "", "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2017-08-10 15:34:09.409562", "payment_term",
"custom": 0, "section_break_13",
"docstatus": 0, "description",
"doctype": "DocType", "section_break_4",
"document_type": "", "invoice_portion",
"editable_grid": 1, "mode_of_payment",
"engine": "InnoDB", "column_break_3",
"due_date_based_on",
"credit_days",
"credit_months",
"section_break_8",
"discount_type",
"discount",
"column_break_11",
"discount_validity_based_on",
"discount_validity"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "fieldname": "payment_term",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Payment Term",
"columns": 2, "options": "Payment Term"
"fieldname": "payment_term", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Term",
"length": 0,
"no_copy": 0,
"options": "Payment Term",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "fetch_from": "payment_term.description",
"allow_on_submit": 0, "fieldname": "description",
"bold": 0, "fieldtype": "Small Text",
"collapsible": 0, "in_list_view": 1,
"columns": 2, "label": "Description"
"fieldname": "description", },
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "fetch_from": "payment_term.invoice_portion",
"allow_on_submit": 0, "fetch_if_empty": 1,
"bold": 0, "fieldname": "invoice_portion",
"collapsible": 0, "fieldtype": "Float",
"columns": 2, "in_list_view": 1,
"default": "0", "label": "Invoice Portion (%)",
"fieldname": "invoice_portion", "reqd": 1
"fieldtype": "Percent", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Invoice Portion",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "fetch_from": "payment_term.due_date_based_on",
"allow_on_submit": 0, "fetch_if_empty": 1,
"bold": 0, "fieldname": "due_date_based_on",
"collapsible": 0, "fieldtype": "Select",
"columns": 2, "in_list_view": 1,
"fieldname": "due_date_based_on", "label": "Due Date Based On",
"fieldtype": "Select", "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"hidden": 0, "reqd": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Due Date Based On",
"length": 0,
"no_copy": 0,
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "default": "0",
"allow_on_submit": 0, "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"bold": 0, "fetch_from": "payment_term.credit_days",
"collapsible": 0, "fetch_if_empty": 1,
"columns": 2, "fieldname": "credit_days",
"default": "0", "fieldtype": "Int",
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", "in_list_view": 1,
"fieldname": "credit_days", "label": "Credit Days",
"fieldtype": "Int", "non_negative": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Credit Days",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"allow_on_submit": 0, "fetch_from": "payment_term.credit_months",
"bold": 0, "fetch_if_empty": 1,
"collapsible": 0, "fieldname": "credit_months",
"columns": 0, "fieldtype": "Int",
"default": "0", "label": "Credit Months",
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", "non_negative": 1
"fieldname": "credit_months", },
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Credit Months",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fetch_from": "payment_term.mode_of_payment",
"allow_in_quick_entry": 0, "fieldname": "mode_of_payment",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Mode of Payment",
"collapsible": 0, "options": "Mode of Payment"
"columns": 0, },
"fieldname": "mode_of_payment", {
"fieldtype": "Link", "fieldname": "column_break_3",
"hidden": 0, "fieldtype": "Column Break"
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0, {
"in_filter": 0, "fieldname": "section_break_8",
"in_global_search": 0, "fieldtype": "Section Break",
"in_list_view": 0, "label": "Discount Settings"
"in_standard_filter": 0, },
"label": "Mode of Payment", {
"length": 0, "default": "Percentage",
"no_copy": 0, "fetch_from": "payment_term.discount_type",
"options": "Mode of Payment", "fetch_if_empty": 1,
"permlevel": 0, "fieldname": "discount_type",
"precision": "", "fieldtype": "Select",
"print_hide": 0, "label": "Discount Type",
"print_hide_if_no_value": 0, "options": "Percentage\nAmount"
"read_only": 0, },
"remember_last_selected_value": 0, {
"report_hide": 0, "fetch_from": "payment_term.discount",
"reqd": 0, "fetch_if_empty": 1,
"search_index": 0, "fieldname": "discount",
"set_only_once": 0, "fieldtype": "Float",
"translatable": 0, "label": "Discount"
"unique": 0 },
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Day(s) after invoice date",
"depends_on": "discount",
"fetch_from": "payment_term.discount_validity_based_on",
"fetch_if_empty": 1,
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
},
{
"collapsible": 1,
"fieldname": "section_break_13",
"fieldtype": "Section Break",
"label": "Description"
},
{
"depends_on": "discount",
"fetch_from": "payment_term.discount_validity",
"fetch_if_empty": 1,
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"mandatory_depends_on": "discount"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "istable": 1,
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2021-02-24 11:56:12.410807",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Accounts",
"is_submittable": 0, "name": "Payment Terms Template Detail",
"issingle": 0, "owner": "Administrator",
"istable": 1, "permissions": [],
"max_attachments": 0, "quick_entry": 1,
"modified": "2018-08-21 16:15:55.143025", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Accounts", "track_changes": 1
"name": "Payment Terms Template Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@ -18,7 +18,7 @@ class POSClosingEntry(StatusUpdater):
self.validate_pos_closing() self.validate_pos_closing()
self.validate_pos_invoices() self.validate_pos_invoices()
def validate_pos_closing(self): def validate_pos_closing(self):
user = frappe.db.sql(""" user = frappe.db.sql("""
SELECT name FROM `tabPOS Closing Entry` SELECT name FROM `tabPOS Closing Entry`
@ -37,12 +37,12 @@ class POSClosingEntry(StatusUpdater):
bold_user = frappe.bold(self.user) bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period") frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period")) .format(bold_already_exists, bold_user), title=_("Invalid Period"))
def validate_pos_invoices(self): def validate_pos_invoices(self):
invalid_rows = [] invalid_rows = []
for d in self.pos_transactions: for d in self.pos_transactions:
invalid_row = {'idx': d.idx} invalid_row = {'idx': d.idx}
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0] ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
if pos_invoice.consolidated_invoice: if pos_invoice.consolidated_invoice:
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated"))) invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
@ -68,14 +68,15 @@ class POSClosingEntry(StatusUpdater):
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
@frappe.whitelist()
def get_payment_reconciliation_details(self): def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency") currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"data": self, "currency": currency}) {"data": self, "currency": currency})
def on_submit(self): def on_submit(self):
consolidate_pos_invoices(closing_entry=self) consolidate_pos_invoices(closing_entry=self)
def on_cancel(self): def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self) unconsolidate_pos_invoices(closing_entry=self)

View File

@ -355,6 +355,7 @@ class POSInvoice(SalesInvoice):
return profile return profile
@frappe.whitelist()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
profile = self.set_pos_fields(for_validate) profile = self.set_pos_fields(for_validate)
@ -377,6 +378,7 @@ class POSInvoice(SalesInvoice):
"allow_print_before_pay": profile.get("allow_print_before_pay") "allow_print_before_pay": profile.get("allow_print_before_pay")
} }
@frappe.whitelist()
def reset_mode_of_payments(self): def reset_mode_of_payments(self):
if self.pos_profile: if self.pos_profile:
pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile) pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile)
@ -389,6 +391,7 @@ class POSInvoice(SalesInvoice):
if not pay.account: if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@frappe.whitelist()
def create_payment_request(self): def create_payment_request(self):
for pay in self.payments: for pay in self.payments:
if pay.type == "Phone": if pay.type == "Phone":

View File

@ -12,6 +12,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
class TestPOSInvoice(unittest.TestCase): class TestPOSInvoice(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
def tearDown(self): def tearDown(self):
if frappe.session.user != "Administrator": if frappe.session.user != "Administrator":
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@ -12,6 +12,7 @@ from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info
import json
from six import iteritems from six import iteritems
@ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document):
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1 sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save() sales_invoice.save()
sales_invoice.submit() sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name self.consolidated_invoice = sales_invoice.name
return sales_invoice.name return sales_invoice.name
@ -91,10 +95,13 @@ class POSInvoiceMergeLog(Document):
credit_note = self.merge_pos_invoice_into(credit_note, data) credit_note = self.merge_pos_invoice_into(credit_note, data)
credit_note.is_consolidated = 1 credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
# TODO: return could be against multiple sales invoice which could also have been consolidated? # TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice # credit_note.return_against = self.consolidated_invoice
credit_note.save() credit_note.save()
credit_note.submit() credit_note.submit()
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
return credit_note.name return credit_note.name
@ -131,12 +138,14 @@ class POSInvoiceMergeLog(Document):
if t.account_head == tax.account_head and t.cost_center == tax.cost_center: if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True found = True
if not found: if not found:
tax.charge_type = 'Actual' tax.charge_type = 'Actual'
tax.included_in_print_rate = 0 tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
taxes.append(tax) taxes.append(tax)
for payment in doc.get('payments'): for payment in doc.get('payments'):
@ -168,11 +177,9 @@ class POSInvoiceMergeLog(Document):
sales_invoice = frappe.new_doc('Sales Invoice') sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer sales_invoice.customer = self.customer
sales_invoice.is_pos = 1 sales_invoice.is_pos = 1
# date can be pos closing date?
sales_invoice.posting_date = getdate(nowdate())
return sales_invoice return sales_invoice
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''): def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for doc in invoice_docs: for doc in invoice_docs:
doc.load_from_db() doc.load_from_db()
@ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document):
si.flags.ignore_validate = True si.flags.ignore_validate = True
si.cancel() si.cancel()
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
if not consolidated_tax_detail:
consolidated_tax_detail = {}
for item_code, tax_data in tax_row_detail.items():
if consolidated_tax_detail.get(item_code):
consolidated_tax_data = consolidated_tax_detail.get(item_code)
consolidated_tax_detail.update({
item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
})
else:
consolidated_tax_detail.update({
item_code: [tax_data[0], tax_data[1]]
})
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
def get_all_unconsolidated_invoices(): def get_all_unconsolidated_invoices():
filters = { filters = {
'consolidated_invoice': [ 'in', [ '', None ]], 'consolidated_invoice': [ 'in', [ '', None ]],
@ -214,7 +241,7 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
if len(invoices) >= 5 and closing_entry: if len(invoices) >= 5 and closing_entry:
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry) enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else: else:
create_merge_logs(invoice_by_customer, closing_entry) create_merge_logs(invoice_by_customer, closing_entry)
@ -227,21 +254,21 @@ def unconsolidate_pos_invoices(closing_entry):
if len(merge_logs) >= 5: if len(merge_logs) >= 5:
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs, closing_entry) enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else: else:
cancel_merge_logs(merge_logs, closing_entry) cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}): def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer): for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log') merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(nowdate()) merge_log.posting_date = getdate(closing_entry.get('posting_date'))
merge_log.customer = customer merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None) merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices) merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True) merge_log.save(ignore_permissions=True)
merge_log.submit() merge_log.submit()
if closing_entry: if closing_entry:
closing_entry.set_status(update=True, status='Submitted') closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry() closing_entry.update_opening_entry()
@ -256,7 +283,7 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
closing_entry.set_status(update=True, status='Cancelled') closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True) closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, invoice_by_customer, closing_entry): def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
check_scheduler_status() check_scheduler_status()
job_name = closing_entry.get("name") job_name = closing_entry.get("name")
@ -269,6 +296,7 @@ def enqueue_job(job, invoice_by_customer, closing_entry):
job_name=job_name, job_name=job_name,
closing_entry=closing_entry, closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer, invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test now=frappe.conf.developer_mode or frappe.flags.in_test
) )

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import json
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
@ -99,4 +100,51 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidated_invoice_item_taxes(self):
frappe.db.sql("delete from `tabPOS Invoice`")
try:
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 9
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv2.get('items')[0].item_code = '_Test Item 2'
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 5
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
tax_rate, amount = item_wise_tax_detail.get('_Test Item')
self.assertEqual(tax_rate, 9)
self.assertEqual(amount, 9)
tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
self.assertEqual(tax_rate2, 5)
self.assertEqual(amount2, 5)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@ -328,6 +328,21 @@ class TestPricingRule(unittest.TestCase):
self.assertEquals(item.discount_amount, 110) self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990) self.assertEquals(item.rate, 990)
def test_pricing_rule_with_margin_and_discount_amount(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10,
rate_or_discount="Discount Amount", discount_amount=110)
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
si.insert(ignore_permissions=True)
item = si.items[0]
self.assertEquals(item.margin_rate_or_amount, 10)
self.assertEquals(item.rate_with_margin, 1100)
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self): def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = { test_record = {
@ -560,6 +575,7 @@ def make_pricing_rule(**args):
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '', "condition": args.condition or '',
"priority": 1, "priority": 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })

View File

@ -127,7 +127,6 @@
"write_off_cost_center", "write_off_cost_center",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"adjust_advance_taxes",
"get_advances", "get_advances",
"advances", "advances",
"payment_schedule_section", "payment_schedule_section",
@ -1326,13 +1325,6 @@
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
}, },
{
"default": "0",
"description": "Taxes paid while advance payment will be adjusted against this invoice",
"fieldname": "adjust_advance_taxes",
"fieldtype": "Check",
"label": "Adjust Advance Taxes"
},
{ {
"depends_on": "eval:doc.is_internal_supplier", "depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
@ -1378,7 +1370,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-09 21:12:30.422084", "modified": "2021-03-30 21:45:58.334107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -43,7 +43,7 @@
} }
], ],
"grand_total": 0, "grand_total": 0,
"naming_series": "_T-BILL", "naming_series": "T-PINV-",
"taxes": [ "taxes": [
{ {
"account_head": "_Test Account Shipping Charges - _TC", "account_head": "_Test Account Shipping Charges - _TC",
@ -167,7 +167,7 @@
} }
], ],
"grand_total": 0, "grand_total": 0,
"naming_series": "_T-Purchase Invoice-", "naming_series": "T-PINV-",
"taxes": [ "taxes": [
{ {
"account_head": "_Test Account Shipping Charges - _TC", "account_head": "_Test Account Shipping Charges - _TC",

View File

@ -916,7 +916,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
}, },
callback: function(r, rt) { callback: function(r, rt) {
if(r.message){ if(r.message){
data = r.message; let data = r.message;
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours); frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);

View File

@ -1952,13 +1952,12 @@
"is_submittable": 1, "is_submittable": 1,
"links": [ "links": [
{ {
"custom": 1,
"group": "Reference", "group": "Reference",
"link_doctype": "POS Invoice", "link_doctype": "POS Invoice",
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-02-01 15:42:26.261540", "modified": "2021-03-31 15:42:26.261540",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -390,6 +390,7 @@ class SalesInvoice(SellingController):
if validate_against_credit_limit: if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order) check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
@frappe.whitelist()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate) pos = self.set_pos_fields(for_validate)
@ -729,6 +730,7 @@ class SalesInvoice(SellingController):
else: else:
self.calculate_billing_amount_for_timesheet() self.calculate_billing_amount_for_timesheet()
@frappe.whitelist()
def add_timesheet_data(self): def add_timesheet_data(self):
self.set('timesheets', []) self.set('timesheets', [])
if self.project: if self.project:
@ -1286,6 +1288,7 @@ class SalesInvoice(SellingController):
break break
# Healthcare # Healthcare
@frappe.whitelist()
def set_healthcare_services(self, checked_values): def set_healthcare_services(self, checked_values):
self.set("items", []) self.set("items", [])
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details

View File

@ -31,7 +31,7 @@
"base_grand_total": 561.8, "base_grand_total": 561.8,
"grand_total": 561.8, "grand_total": 561.8,
"is_pos": 0, "is_pos": 0,
"naming_series": "_T-Sales Invoice-", "naming_series": "T-SINV-",
"base_net_total": 500.0, "base_net_total": 500.0,
"taxes": [ "taxes": [
{ {
@ -104,7 +104,7 @@
"base_grand_total": 630.0, "base_grand_total": 630.0,
"grand_total": 630.0, "grand_total": 630.0,
"is_pos": 0, "is_pos": 0,
"naming_series": "_T-Sales Invoice-", "naming_series": "T-SINV-",
"base_net_total": 500.0, "base_net_total": 500.0,
"taxes": [ "taxes": [
{ {
@ -175,7 +175,7 @@
], ],
"grand_total": 0, "grand_total": 0,
"is_pos": 0, "is_pos": 0,
"naming_series": "_T-Sales Invoice-", "naming_series": "T-SINV-",
"taxes": [ "taxes": [
{ {
"account_head": "_Test Account Shipping Charges - _TC", "account_head": "_Test Account Shipping Charges - _TC",
@ -301,7 +301,7 @@
], ],
"grand_total": 0, "grand_total": 0,
"is_pos": 0, "is_pos": 0,
"naming_series": "_T-Sales Invoice-", "naming_series": "T-SINV-",
"taxes": [ "taxes": [
{ {
"account_head": "_Test Account Excise Duty - _TC", "account_head": "_Test Account Excise Duty - _TC",

View File

@ -2115,6 +2115,7 @@ def create_sales_invoice(**args):
si.return_against = args.return_against si.return_against = args.return_against
si.currency=args.currency or "INR" si.currency=args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1 si.conversion_rate = args.conversion_rate or 1
si.naming_series = args.naming_series or "T-SINV-"
si.append("items", { si.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",

View File

@ -14,10 +14,15 @@ test_records = frappe.get_test_records('Tax Rule')
from six import iteritems from six import iteritems
class TestTaxRule(unittest.TestCase): class TestTaxRule(unittest.TestCase):
def setUp(self): @classmethod
def setUpClass(cls):
frappe.db.set_value("Shopping Cart Settings", None, "enabled", 0)
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
def tearDown(self): def setUp(self):
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
def test_conflict(self): def test_conflict(self):

View File

@ -364,7 +364,7 @@ class ReceivablePayableReport(object):
payment_terms_details = frappe.db.sql(""" payment_terms_details = frappe.db.sql("""
select select
si.name, si.party_account_currency, si.currency, si.conversion_rate, si.name, si.party_account_currency, si.currency, si.conversion_rate,
ps.due_date, ps.payment_amount, ps.description, ps.paid_amount ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps from `tab{0}` si, `tabPayment Schedule` ps
where where
si.name = ps.parent and si.name = ps.parent and
@ -395,13 +395,13 @@ class ReceivablePayableReport(object):
"invoiced": invoiced, "invoiced": invoiced,
"invoice_grand_total": row.invoiced, "invoice_grand_total": row.invoiced,
"payment_term": d.description, "payment_term": d.description,
"paid": d.paid_amount, "paid": d.paid_amount + d.discounted_amount,
"credit_note": 0.0, "credit_note": 0.0,
"outstanding": invoiced - d.paid_amount "outstanding": invoiced - d.paid_amount - d.discounted_amount
})) }))
if d.paid_amount: if d.paid_amount:
row['paid'] -= d.paid_amount row['paid'] -= d.paid_amount + d.discounted_amount
def allocate_closing_to_term(self, row, term, key): def allocate_closing_to_term(self, row, term, key):
if row[key]: if row[key]:

View File

@ -406,9 +406,10 @@ def check_if_advance_entry_modified(args):
throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) throw(_("""Payment Entry has been modified after you pulled it. Please pull it again."""))
def validate_allocated_amount(args): def validate_allocated_amount(args):
precision = args.get('precision') or frappe.db.get_single_value("System Settings", "currency_precision")
if args.get("allocated_amount") < 0: if args.get("allocated_amount") < 0:
throw(_("Allocated amount cannot be negative")) throw(_("Allocated amount cannot be negative"))
elif args.get("allocated_amount") > args.get("unadjusted_amount"): elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision):
throw(_("Allocated amount cannot be greater than unadjusted amount")) throw(_("Allocated amount cannot be greater than unadjusted amount"))
def update_reference_in_journal_entry(d, jv_obj): def update_reference_in_journal_entry(d, jv_obj):

View File

@ -71,6 +71,7 @@ class CropCycle(Document):
"exp_end_date": add_days(start_date, crop_task.get("end_day") - 1) "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
}).insert() }).insert()
@frappe.whitelist()
def reload_linked_analysis(self): def reload_linked_analysis(self):
linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis'] linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
required_fields = ['location', 'name', 'collection_datetime'] required_fields = ['location', 'name', 'collection_datetime']
@ -87,6 +88,7 @@ class CropCycle(Document):
frappe.publish_realtime("List of Linked Docs", frappe.publish_realtime("List of Linked Docs",
output, user=frappe.session.user) output, user=frappe.session.user)
@frappe.whitelist()
def append_to_child(self, obj_to_append): def append_to_child(self, obj_to_append):
for doctype in obj_to_append: for doctype in obj_to_append:
for doc_name in set(obj_to_append[doctype]): for doc_name in set(obj_to_append[doctype]):

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Fertilizer(Document): class Fertilizer(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'})
for doc in docs: for doc in docs:

View File

@ -8,6 +8,7 @@ from frappe.model.naming import make_autoname
from frappe.model.document import Document from frappe.model.document import Document
class PlantAnalysis(Document): class PlantAnalysis(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'})
for doc in docs: for doc in docs:

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class SoilAnalysis(Document): class SoilAnalysis(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'})
for doc in docs: for doc in docs:

View File

@ -13,6 +13,7 @@ class SoilTexture(Document):
soil_edit_order = [2, 1, 0] soil_edit_order = [2, 1, 0]
soil_types = ['clay_composition', 'sand_composition', 'silt_composition'] soil_types = ['clay_composition', 'sand_composition', 'silt_composition']
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'})
for doc in docs: for doc in docs:
@ -26,6 +27,7 @@ class SoilTexture(Document):
if sum(self.get(soil_type) for soil_type in self.soil_types) != 100: if sum(self.get(soil_type) for soil_type in self.soil_types) != 100:
frappe.throw(_('Soil compositions do not add up to 100')) frappe.throw(_('Soil compositions do not add up to 100'))
@frappe.whitelist()
def update_soil_edit(self, soil_type): def update_soil_edit(self, soil_type):
self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1 self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1
self.soil_type = self.get_soil_type() self.soil_type = self.get_soil_type()
@ -35,8 +37,8 @@ class SoilTexture(Document):
if sum(self.soil_edit_order) < 5: return if sum(self.soil_edit_order) < 5: return
last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order)) last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order))
# set composition of the last edited soil # set composition of the last edited soil
self.set( self.soil_types[last_edit_index], self.set(self.soil_types[last_edit_index],
100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index]))) 100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index])))
# calculate soil type # calculate soil type
@ -67,4 +69,4 @@ class SoilTexture(Document):
elif (c >= 40 and sa <= 45 and si < 40): elif (c >= 40 and sa <= 45 and si < 40):
return 'Clay' return 'Clay'
else: else:
return 'Select' return 'Select'

View File

@ -9,11 +9,13 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
class WaterAnalysis(Document): class WaterAnalysis(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'})
for doc in docs: for doc in docs:
self.append('water_analysis_criteria', {'title': str(doc.name)}) self.append('water_analysis_criteria', {'title': str(doc.name)})
@frappe.whitelist()
def update_lab_result_date(self): def update_lab_result_date(self):
if not self.result_datetime: if not self.result_datetime:
self.result_datetime = self.laboratory_testing_datetime self.result_datetime = self.laboratory_testing_datetime

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Weather(Document): class Weather(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'})
for doc in docs: for doc in docs:

View File

@ -553,6 +553,7 @@ class Asset(AccountsController):
make_gl_entries(gl_entries) make_gl_entries(gl_entries)
self.db_set('booked_fixed_asset', 1) self.db_set('booked_fixed_asset', 1)
@frappe.whitelist()
def get_depreciation_rate(self, args, on_validate=False): def get_depreciation_rate(self, args, on_validate=False):
if isinstance(args, string_types): if isinstance(args, string_types):
args = json.loads(args) args = json.loads(args)

View File

@ -133,6 +133,7 @@ class PurchaseOrder(BuyingController):
d.material_request_item, "schedule_date") d.material_request_item, "schedule_date")
@frappe.whitelist()
def get_last_purchase_rate(self): def get_last_purchase_rate(self):
"""get last purchase rates for all items""" """get last purchase rates for all items"""
@ -252,6 +253,7 @@ class PurchaseOrder(BuyingController):
self.update_prevdoc_status() self.update_prevdoc_status()
# Must be called after updating ordered qty in Material Request # Must be called after updating ordered qty in Material Request
# bin uses Material Request Items to recalculate & update
self.update_requested_qty() self.update_requested_qty()
self.update_ordered_qty() self.update_ordered_qty()
@ -366,7 +368,6 @@ def make_purchase_receipt(source_name, target_doc=None):
"Purchase Order": { "Purchase Order": {
"doctype": "Purchase Receipt", "doctype": "Purchase Receipt",
"field_map": { "field_map": {
"per_billed": "per_billed",
"supplier_warehouse":"supplier_warehouse" "supplier_warehouse":"supplier_warehouse"
}, },
"validation": { "validation": {

View File

@ -90,6 +90,50 @@ class TestPurchaseOrder(unittest.TestCase):
frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 0) frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 0)
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
def test_update_remove_child_linked_to_mr(self):
"""Test impact on linked PO and MR on deleting/updating row."""
mr = make_material_request(qty=10)
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.save()
po.submit()
first_item_of_po = po.get("items")[0]
existing_ordered_qty = get_ordered_qty() # 10
existing_requested_qty = get_requested_qty() # 0
# decrease ordered qty by 3 (10 -> 7) and add item
trans_item = json.dumps([
{
'item_code': first_item_of_po.item_code,
'rate': first_item_of_po.rate,
'qty': 7,
'docname': first_item_of_po.name
},
{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2}
])
update_child_qty_rate('Purchase Order', trans_item, po.name)
mr.reload()
# requested qty increases as ordered qty decreases
self.assertEqual(get_requested_qty(), existing_requested_qty + 3) # 3
self.assertEqual(mr.items[0].ordered_qty, 7)
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 3) # 7
# delete first item linked to Material Request
trans_item = json.dumps([
{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2}
])
update_child_qty_rate('Purchase Order', trans_item, po.name)
mr.reload()
# requested qty increases as ordered qty is 0 (deleted row)
self.assertEqual(get_requested_qty(), existing_requested_qty + 10) # 10
self.assertEqual(mr.items[0].ordered_qty, 0)
# ordered qty decreases as ordered qty is 0 (deleted row)
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
def test_update_child(self): def test_update_child(self):
mr = make_material_request(qty=10) mr = make_material_request(qty=10)
@ -120,7 +164,6 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.get("items")[0].amount, 1400) self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
def test_update_child_adding_new_item(self): def test_update_child_adding_new_item(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4 po.items[0].qty = 4
@ -129,6 +172,7 @@ class TestPurchaseOrder(unittest.TestCase):
pr = make_pr_against_po(po.name, 2) pr = make_pr_against_po(po.name, 2)
po.load_from_db() po.load_from_db()
existing_ordered_qty = get_ordered_qty()
first_item_of_po = po.get("items")[0] first_item_of_po = po.get("items")[0]
trans_item = json.dumps([ trans_item = json.dumps([
@ -145,7 +189,8 @@ class TestPurchaseOrder(unittest.TestCase):
po.reload() po.reload()
self.assertEquals(len(po.get('items')), 2) self.assertEquals(len(po.get('items')), 2)
self.assertEqual(po.status, 'To Receive and Bill') self.assertEqual(po.status, 'To Receive and Bill')
# ordered qty should increase on row addition
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
def test_update_child_removing_item(self): def test_update_child_removing_item(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
@ -156,6 +201,7 @@ class TestPurchaseOrder(unittest.TestCase):
po.reload() po.reload()
first_item_of_po = po.get("items")[0] first_item_of_po = po.get("items")[0]
existing_ordered_qty = get_ordered_qty()
# add an item # add an item
trans_item = json.dumps([ trans_item = json.dumps([
{ {
@ -168,6 +214,10 @@ class TestPurchaseOrder(unittest.TestCase):
update_child_qty_rate('Purchase Order', trans_item, po.name) update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload() po.reload()
# ordered qty should increase on row addition
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
# check if can remove received item # check if can remove received item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}]) trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name)
@ -187,6 +237,9 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEquals(len(po.get('items')), 1) self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill') self.assertEqual(po.status, 'To Receive and Bill')
# ordered qty should decrease (back to initial) on row deletion
self.assertEqual(get_ordered_qty(), existing_ordered_qty)
def test_update_child_perm(self): def test_update_child_perm(self):
po = create_purchase_order(item_code= "_Test Item", qty=4) po = create_purchase_order(item_code= "_Test Item", qty=4)
@ -230,11 +283,13 @@ class TestPurchaseOrder(unittest.TestCase):
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", { if not frappe.db.exists("Item Tax",
"item_tax_template": "Test Update Items Template - _TC", {"item_tax_template": "Test Update Items Template - _TC", "parent": "Test Item with Tax"}):
"valid_from": nowdate() new_item_with_tax.append("taxes", {
}) "item_tax_template": "Test Update Items Template - _TC",
new_item_with_tax.save() "valid_from": nowdate()
})
new_item_with_tax.save()
tax_template = "_Test Account Excise Duty @ 10 - _TC" tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100" item = "_Test Item Home Desktop 100"

View File

@ -66,6 +66,7 @@ class RequestforQuotation(BuyingController):
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
@frappe.whitelist()
def get_supplier_email_preview(self, supplier): def get_supplier_email_preview(self, supplier):
"""Returns formatted email preview as string.""" """Returns formatted email preview as string."""
rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers)) rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))

View File

@ -9,9 +9,7 @@ import unittest
class TestSupplierScorecard(unittest.TestCase): class TestSupplierScorecard(unittest.TestCase):
def test_create_scorecard(self): def test_create_scorecard(self):
delete_test_scorecards() doc = make_supplier_scorecard().insert()
my_doc = make_supplier_scorecard()
doc = my_doc.insert()
self.assertEqual(doc.name, valid_scorecard[0].get("supplier")) self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
def test_criteria_weight(self): def test_criteria_weight(self):
@ -121,7 +119,8 @@ valid_scorecard = [
{ {
"weight":100.0, "weight":100.0,
"doctype":"Supplier Scorecard Scoring Criteria", "doctype":"Supplier Scorecard Scoring Criteria",
"criteria_name":"Delivery" "criteria_name":"Delivery",
"formula": "100"
} }
], ],
"supplier":"_Test Supplier", "supplier":"_Test Supplier",

View File

@ -517,6 +517,7 @@ class AccountsController(TransactionBase):
frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s
and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name)) and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name))
@frappe.whitelist()
def apply_shipping_rule(self): def apply_shipping_rule(self):
if self.shipping_rule: if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule) shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)
@ -537,6 +538,7 @@ class AccountsController(TransactionBase):
return {} return {}
@frappe.whitelist()
def set_advances(self): def set_advances(self):
"""Returns list of advances against Account, Party, Reference""" """Returns list of advances against Account, Party, Reference"""
@ -657,6 +659,7 @@ class AccountsController(TransactionBase):
'dr_or_cr': dr_or_cr, 'dr_or_cr': dr_or_cr,
'unadjusted_amount': flt(d.advance_amount), 'unadjusted_amount': flt(d.advance_amount),
'allocated_amount': flt(d.allocated_amount), 'allocated_amount': flt(d.allocated_amount),
'precision': d.precision('advance_amount'),
'exchange_rate': (self.conversion_rate 'exchange_rate': (self.conversion_rate
if self.party_account_currency != self.company_currency else 1), if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total 'grand_total': (self.base_grand_total
@ -921,7 +924,8 @@ class AccountsController(TransactionBase):
else: else:
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
if d.invoice_portion: if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount')) d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.outstanding = d.payment_amount
def set_due_date(self): def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@ -1236,18 +1240,24 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, bill_dat
term_details.description = term.description term_details.description = term.description
term_details.invoice_portion = term.invoice_portion term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100 term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
term_details.discount_type = term.discount_type
term_details.discount = term.discount
# term_details.discounted_amount = flt(grand_total) * (term.discount / 100) if term.discount_type == 'Percentage' else discount
term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment
if bill_date: if bill_date:
term_details.due_date = get_due_date(term, bill_date) term_details.due_date = get_due_date(term, bill_date)
term_details.discount_date = get_discount_date(term, bill_date)
elif posting_date: elif posting_date:
term_details.due_date = get_due_date(term, posting_date) term_details.due_date = get_due_date(term, posting_date)
term_details.discount_date = get_discount_date(term, posting_date)
if getdate(term_details.due_date) < getdate(posting_date): if getdate(term_details.due_date) < getdate(posting_date):
term_details.due_date = posting_date term_details.due_date = posting_date
term_details.mode_of_payment = term.mode_of_payment
return term_details return term_details
def get_due_date(term, posting_date=None, bill_date=None): def get_due_date(term, posting_date=None, bill_date=None):
due_date = None due_date = None
date = bill_date or posting_date date = bill_date or posting_date
@ -1259,6 +1269,16 @@ def get_due_date(term, posting_date=None, bill_date=None):
due_date = add_months(get_last_day(date), term.credit_months) due_date = add_months(get_last_day(date), term.credit_months)
return due_date return due_date
def get_discount_date(term, posting_date=None, bill_date=None):
discount_validity = None
date = bill_date or posting_date
if term.discount_validity_based_on == "Day(s) after invoice date":
discount_validity = add_days(date, term.discount_validity)
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
discount_validity = add_days(get_last_day(date), term.discount_validity)
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
discount_validity = add_months(get_last_day(date), term.discount_validity)
return discount_validity
def get_supplier_block_status(party_name): def get_supplier_block_status(party_name):
""" """
@ -1317,25 +1337,63 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc(child_doctype, p_doc, child_docname) child_item = frappe.new_doc(child_doctype, p_doc, child_docname)
item = frappe.get_doc("Item", trans_item.get('item_code')) item = frappe.get_doc("Item", trans_item.get('item_code'))
for field in ("item_code", "item_name", "description", "item_group"): for field in ("item_code", "item_name", "description", "item_group"):
child_item.update({field: item.get(field)}) child_item.update({field: item.get(field)})
date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date" date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)}) child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
child_item.stock_uom = item.stock_uom
child_item.uom = trans_item.get("uom") or item.stock_uom child_item.uom = trans_item.get("uom") or item.stock_uom
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
if child_doctype == "Purchase Order Item": if child_doctype == "Purchase Order Item":
child_item.base_rate = 1 # Initiallize value will update in parent validation # Initialized value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_rate = 1
child_item.base_amount = 1
if child_doctype == "Sales Order Item": if child_doctype == "Sales Order Item":
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse: if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
.format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) .format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
set_child_tax_template_and_map(item, child_item, p_doc) set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc) add_taxes_from_tax_template(child_item, p_doc)
return child_item return child_item
def validate_child_on_delete(row, parent):
"""Check if partially transacted item (row) is being deleted."""
if parent.doctype == "Sales Order":
if flt(row.delivered_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(row.idx, row.item_code))
if flt(row.work_order_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(row.idx, row.item_code))
if flt(row.ordered_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(row.idx, row.item_code))
if parent.doctype == "Purchase Order" and flt(row.received_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(row.idx, row.item_code))
if flt(row.billed_amt):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(row.idx, row.item_code))
def update_bin_on_delete(row, doctype):
"""Update bin for deleted item (row)."""
from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty, get_ordered_qty, get_indented_qty
qty_dict = {}
if doctype == "Sales Order":
qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse)
else:
if row.material_request_item:
qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse)
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data): def validate_and_delete_children(parent, data):
deleted_children = [] deleted_children = []
updated_item_names = [d.get("docname") for d in data] updated_item_names = [d.get("docname") for d in data]
@ -1344,23 +1402,17 @@ def validate_and_delete_children(parent, data):
deleted_children.append(item) deleted_children.append(item)
for d in deleted_children: for d in deleted_children:
if parent.doctype == "Sales Order": validate_child_on_delete(d, parent)
if flt(d.delivered_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code))
if flt(d.work_order_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(d.idx, d.item_code))
if flt(d.ordered_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(d.idx, d.item_code))
if parent.doctype == "Purchase Order" and flt(d.received_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code))
if flt(d.billed_amt):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code))
d.cancel() d.cancel()
d.delete() d.delete()
# need to update ordered qty in Material Request first
# bin uses Material Request Items to recalculate & update
parent.update_prevdoc_status()
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'): def check_doc_permissions(doc, perm_type='create'):

View File

@ -406,8 +406,7 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self): def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"): for d in self.get("items"):
if d.conversion_factor: d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self): def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \

View File

@ -113,10 +113,10 @@ class calculate_taxes_and_totals(object):
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0: if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
if not item.discount_amount: if item.discount_amount and not item.discount_percentage:
item.discount_amount = item.rate_with_margin - item.rate
elif not item.discount_percentage:
item.rate -= item.discount_amount item.rate -= item.discount_amount
else:
item.discount_amount = item.rate_with_margin - item.rate
elif flt(item.price_list_rate) > 0: elif flt(item.price_list_rate) > 0:
item.discount_amount = item.price_list_rate - item.rate item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount: elif flt(item.price_list_rate) > 0 and not item.discount_amount:
@ -147,7 +147,9 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) validate_inclusive_tax(tax, self.doc)
tax.item_wise_tax_detail = {} if not self.doc.get('is_consolidated'):
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount", tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item", "tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
@ -338,7 +340,9 @@ class calculate_taxes_and_totals(object):
current_tax_amount = tax_rate * item.qty current_tax_amount = tax_rate * item.qty
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount) current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
if not self.doc.get("is_consolidated"):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount return current_tax_amount
@ -440,8 +444,9 @@ class calculate_taxes_and_totals(object):
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self): def _cleanup(self):
for tax in self.doc.get("taxes"): if not self.doc.get('is_consolidated'):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.additional_discount_percentage:
@ -808,4 +813,4 @@ class init_landed_taxes_and_totals(object):
def set_amounts_in_company_currency(self): def set_amounts_in_company_currency(self):
for d in self.doc.get(self.tax_field): for d in self.doc.get(self.tax_field):
d.amount = flt(d.amount, d.precision("amount")) d.amount = flt(d.amount, d.precision("amount"))
d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Lead Source', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,62 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:source_name",
"creation": "2016-09-16 01:47:47.382372",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source_name",
"details"
],
"fields": [
{
"fieldname": "source_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Source Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "details",
"fieldtype": "Text Editor",
"label": "Details"
}
],
"links": [],
"modified": "2021-02-08 12:51:48.971517",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead Source",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

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

View File

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe # import frappe
import unittest import unittest
# test_records = frappe.get_test_records('Lead Source')
class TestLeadSource(unittest.TestCase): class TestLeadSource(unittest.TestCase):
pass pass

View File

@ -11,7 +11,8 @@ from frappe.utils.file_manager import get_file, get_file_path
from six.moves.urllib.parse import urlencode from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document): class LinkedInSettings(Document):
def get_authorization_url(self): @frappe.whitelist()
def get_authorization_url(self):
params = urlencode({ params = urlencode({
"response_type":"code", "response_type":"code",
"client_id": self.consumer_key, "client_id": self.consumer_key,
@ -35,7 +36,7 @@ class LinkedInSettings(Document):
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
} }
response = self.http_post(url=url, data=body, headers=headers) response = self.http_post(url=url, data=body, headers=headers)
response = frappe.parse_json(response.content.decode()) response = frappe.parse_json(response.content.decode())
self.db_set("access_token", response["access_token"]) self.db_set("access_token", response["access_token"])

View File

@ -85,6 +85,7 @@ class Opportunity(TransactionBase):
self.opportunity_from = "Lead" self.opportunity_from = "Lead"
self.party_name = lead_name self.party_name = lead_name
@frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_active_quotation(): if not self.has_active_quotation():
frappe.db.set(self, 'status', 'Lost') frappe.db.set(self, 'status', 'Lost')

View File

@ -11,6 +11,7 @@ from frappe.utils import get_url_to_form, get_link_to_form
from tweepy.error import TweepError from tweepy.error import TweepError
class TwitterSettings(Document): class TwitterSettings(Document):
@frappe.whitelist()
def get_authorize_url(self): def get_authorize_url(self):
callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url()) callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url())
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
@ -21,12 +22,12 @@ class TwitterSettings(Document):
frappe.msgprint(_("Error! Failed to get request token.")) frappe.msgprint(_("Error! Failed to get request token."))
frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
def get_access_token(self, oauth_token, oauth_verifier): def get_access_token(self, oauth_token, oauth_verifier):
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
auth.request_token = { auth.request_token = {
'oauth_token' : oauth_token, 'oauth_token' : oauth_token,
'oauth_token_secret' : oauth_verifier 'oauth_token_secret' : oauth_verifier
} }
try: try:
@ -50,10 +51,10 @@ class TwitterSettings(Document):
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
def get_api(self, access_token, access_token_secret): def get_api(self, access_token, access_token_secret):
# authentication of consumer key and secret # authentication of consumer key and secret
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
# authentication of access token and secret # authentication of access token and secret
auth.set_access_token(access_token, access_token_secret) auth.set_access_token(access_token, access_token_secret)
return tweepy.API(auth) return tweepy.API(auth)
@ -64,7 +65,7 @@ class TwitterSettings(Document):
if media: if media:
media_id = self.upload_image(media) media_id = self.upload_image(media)
return self.send_tweet(text, media_id) return self.send_tweet(text, media_id)
def upload_image(self, media): def upload_image(self, media):
media = get_file_path(media) media = get_file_path(media)
api = self.get_api(self.access_token, self.access_token_secret) api = self.get_api(self.access_token, self.access_token_secret)

View File

@ -13,6 +13,7 @@ from erpnext.education.utils import OverlapError
class CourseSchedulingTool(Document): class CourseSchedulingTool(Document):
@frappe.whitelist()
def schedule_course(self): def schedule_course(self):
"""Creates course schedules as per specified parameters""" """Creates course schedules as per specified parameters"""

View File

@ -52,6 +52,7 @@ class FeeSchedule(Document):
self.grand_total = no_of_students*self.total_amount self.grand_total = no_of_students*self.total_amount
self.grand_total_in_words = money_in_words(self.grand_total) self.grand_total_in_words = money_in_words(self.grand_total)
@frappe.whitelist()
def create_fees(self): def create_fees(self):
self.db_set("fee_creation_status", "In Process") self.db_set("fee_creation_status", "In Process")
frappe.publish_realtime("fee_schedule_progress", frappe.publish_realtime("fee_schedule_progress",

View File

@ -91,6 +91,8 @@ class ProgramEnrollment(Document):
(fee, fee) for fee in fee_list] (fee, fee) for fee in fee_list]
msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list))) msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list)))
@frappe.whitelist()
def get_courses(self): def get_courses(self):
return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1) return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1)

View File

@ -14,6 +14,7 @@ class ProgramEnrollmentTool(Document):
academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd')) academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd'))
self.set_onload("academic_term_reqd", academic_term_reqd) self.set_onload("academic_term_reqd", academic_term_reqd)
@frappe.whitelist()
def get_students(self): def get_students(self):
students = [] students = []
if not self.get_students_from: if not self.get_students_from:
@ -49,6 +50,7 @@ class ProgramEnrollmentTool(Document):
else: else:
frappe.throw(_("No students Found")) frappe.throw(_("No students Found"))
@frappe.whitelist()
def enroll_students(self): def enroll_students(self):
total = len(self.students) total = len(self.students)
for i, stud in enumerate(self.students): for i, stud in enumerate(self.students):

View File

@ -9,6 +9,7 @@ from frappe.model.document import Document
from erpnext.education.doctype.student_group.student_group import get_students from erpnext.education.doctype.student_group.student_group import get_students
class StudentGroupCreationTool(Document): class StudentGroupCreationTool(Document):
@frappe.whitelist()
def get_courses(self): def get_courses(self):
group_list = [] group_list = []
@ -42,6 +43,7 @@ class StudentGroupCreationTool(Document):
return group_list return group_list
@frappe.whitelist()
def create_student_groups(self): def create_student_groups(self):
if not self.courses: if not self.courses:
frappe.throw(_("""No Student Groups created.""")) frappe.throw(_("""No Student Groups created."""))

View File

@ -59,9 +59,10 @@ class MpesaSettings(Document):
request_amounts.append(amount) request_amounts.append(amount)
else: else:
request_amounts = [request_amount] request_amounts = [request_amount]
return request_amounts return request_amounts
@frappe.whitelist()
def get_account_balance_info(self): def get_account_balance_info(self):
payload = dict( payload = dict(
reference_doctype="Mpesa Settings", reference_doctype="Mpesa Settings",
@ -198,7 +199,7 @@ def get_completed_integration_requests_info(reference_doctype, reference_docname
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
completed_payments.append(completed_amount) completed_payments.append(completed_amount)
mpesa_receipts.append(completed_mpesa_receipt) mpesa_receipts.append(completed_mpesa_receipt)
return mpesa_receipts, completed_payments return mpesa_receipts, completed_payments
def get_account_balance(request_payload): def get_account_balance(request_payload):

View File

@ -15,6 +15,7 @@ from frappe.utils import add_months, formatdate, getdate, today
class PlaidSettings(Document): class PlaidSettings(Document):
@staticmethod @staticmethod
@frappe.whitelist()
def get_link_token(): def get_link_token():
plaid = PlaidConnector() plaid = PlaidConnector()
return plaid.get_link_token() return plaid.get_link_token()

View File

@ -23,14 +23,9 @@ class TestPlaidSettings(unittest.TestCase):
doc.cancel() doc.cancel()
doc.delete() doc.delete()
for ba in frappe.get_all("Bank Account"): for doctype in ("Bank Account", "Bank Account Type", "Bank Account Subtype"):
frappe.get_doc("Bank Account", ba.name).delete() for d in frappe.get_all(doctype):
frappe.delete_doc(doctype, d.name, force=True)
for at in frappe.get_all("Bank Account Type"):
frappe.get_doc("Bank Account Type", at.name).delete()
for ast in frappe.get_all("Bank Account Subtype"):
frappe.get_doc("Bank Account Subtype", ast.name).delete()
def test_plaid_disabled(self): def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0) frappe.db.set_value("Plaid Settings", None, "enabled", 0)

View File

@ -54,6 +54,7 @@ class QuickBooksMigrator(Document):
self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0]
@frappe.whitelist()
def migrate(self): def migrate(self):
frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long") frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long")

View File

@ -594,18 +594,22 @@ class TallyMigration(Document):
frappe.db.set_value("Price List", "Tally Price List", "enabled", 0) frappe.db.set_value("Price List", "Tally Price List", "enabled", 0)
frappe.flags.in_migrate = False frappe.flags.in_migrate = False
@frappe.whitelist()
def process_master_data(self): def process_master_data(self):
self.set_status("Processing Master Data") self.set_status("Processing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
@frappe.whitelist()
def import_master_data(self): def import_master_data(self):
self.set_status("Importing Master Data") self.set_status("Importing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
@frappe.whitelist()
def process_day_book_data(self): def process_day_book_data(self):
self.set_status("Processing Day Book Data") self.set_status("Processing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
@frappe.whitelist()
def import_day_book_data(self): def import_day_book_data(self):
self.set_status("Importing Day Book Data") self.set_status("Importing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)

View File

@ -54,6 +54,7 @@ class ClinicalProcedure(Document):
def set_title(self): def set_title(self):
self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100] self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100]
@frappe.whitelist()
def complete_procedure(self): def complete_procedure(self):
if self.consume_stock and self.items: if self.consume_stock and self.items:
stock_entry = make_stock_entry(self) stock_entry = make_stock_entry(self)
@ -96,6 +97,7 @@ class ClinicalProcedure(Document):
if self.consume_stock and self.items: if self.consume_stock and self.items:
return stock_entry return stock_entry
@frappe.whitelist()
def start_procedure(self): def start_procedure(self):
allow_start = self.set_actual_qty() allow_start = self.set_actual_qty()
if allow_start: if allow_start:
@ -116,6 +118,7 @@ class ClinicalProcedure(Document):
return allow_start return allow_start
@frappe.whitelist()
def make_material_receipt(self, submit=False): def make_material_receipt(self, submit=False):
stock_entry = frappe.new_doc('Stock Entry') stock_entry = frappe.new_doc('Stock Entry')

View File

@ -14,6 +14,7 @@ class InpatientMedicationEntry(Document):
def validate(self): def validate(self):
self.validate_medication_orders() self.validate_medication_orders()
@frappe.whitelist()
def get_medication_orders(self): def get_medication_orders(self):
# pull inpatient medication orders based on selected filters # pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self) orders = get_pending_medication_orders(self)

View File

@ -57,6 +57,7 @@ class InpatientMedicationOrder(Document):
self.db_set('status', status) self.db_set('status', status)
@frappe.whitelist()
def add_order_entries(self, order): def add_order_entries(self, order):
if order.get('drug_code'): if order.get('drug_code'):
dosage = frappe.get_doc('Prescription Dosage', order.get('dosage')) dosage = frappe.get_doc('Prescription Dosage', order.get('dosage'))

View File

@ -81,15 +81,8 @@ class TestInpatientMedicationOrder(unittest.TestCase):
self.ip_record.reload() self.ip_record.reload()
discharge_patient(self.ip_record) discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'): for doctype in ["Inpatient Medication Entry", "Inpatient Medication Order"]:
doc = frappe.get_doc('Inpatient Medication Entry', entry.name) frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
doc.cancel()
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def create_dosage_form(): def create_dosage_form():
if not frappe.db.exists('Dosage Form', 'Tablet'): if not frappe.db.exists('Dosage Form', 'Tablet'):

View File

@ -53,12 +53,15 @@ class InpatientRecord(Document):
+ """ <b><a href="/app/Form/Inpatient Record/{0}">{0}</a></b>""".format(ip_record[0].name)) + """ <b><a href="/app/Form/Inpatient Record/{0}">{0}</a></b>""".format(ip_record[0].name))
frappe.throw(msg) frappe.throw(msg)
@frappe.whitelist()
def admit(self, service_unit, check_in, expected_discharge=None): def admit(self, service_unit, check_in, expected_discharge=None):
admit_patient(self, service_unit, check_in, expected_discharge) admit_patient(self, service_unit, check_in, expected_discharge)
@frappe.whitelist()
def discharge(self): def discharge(self):
discharge_patient(self) discharge_patient(self)
@frappe.whitelist()
def transfer(self, service_unit, check_in, leave_from): def transfer(self, service_unit, check_in, leave_from):
if leave_from: if leave_from:
patient_leave_service_unit(self, check_in, leave_from) patient_leave_service_unit(self, check_in, leave_from)

View File

@ -111,6 +111,7 @@ class Patient(Document):
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str return age_str
@frappe.whitelist()
def invoice_patient_registration(self): def invoice_patient_registration(self):
if frappe.db.get_single_value('Healthcare Settings', 'registration_fee'): if frappe.db.get_single_value('Healthcare Settings', 'registration_fee'):
company = frappe.defaults.get_user_default('company') company = frappe.defaults.get_user_default('company')

View File

@ -113,6 +113,7 @@ class PatientAppointment(Document):
if fee_validity: if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
@frappe.whitelist()
def get_therapy_types(self): def get_therapy_types(self):
if not self.therapy_plan: if not self.therapy_plan:
return return

View File

@ -34,6 +34,7 @@ class PatientHistorySettings(Document):
frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format( frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
@frappe.whitelist()
def get_doctype_fields(self, document_type, fields): def get_doctype_fields(self, document_type, fields):
multicheck_fields = [] multicheck_fields = []
doc_fields = frappe.get_meta(document_type).fields doc_fields = frappe.get_meta(document_type).fields
@ -49,6 +50,7 @@ class PatientHistorySettings(Document):
return multicheck_fields return multicheck_fields
@frappe.whitelist()
def get_date_field_for_dt(self, document_type): def get_date_field_for_dt(self, document_type):
meta = frappe.get_meta(document_type) meta = frappe.get_meta(document_type)
date_fields = meta.get('fields', { date_fields = meta.get('fields', {

View File

@ -33,6 +33,7 @@ class TherapyPlan(Document):
self.db_set('total_sessions', total_sessions) self.db_set('total_sessions', total_sessions)
self.db_set('total_sessions_completed', total_sessions_completed) self.db_set('total_sessions_completed', total_sessions_completed)
@frappe.whitelist()
def set_therapy_details_from_template(self): def set_therapy_details_from_template(self):
# Add therapy types in the child table # Add therapy types in the child table
self.set('therapy_plan_details', []) self.set('therapy_plan_details', [])

View File

@ -260,7 +260,11 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction" "erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
], ],
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission",
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
]
}, },
"Purchase Invoice": { "Purchase Invoice": {
"validate": [ "validate": [
@ -282,9 +286,6 @@ doc_events = {
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply'] 'validate': ['erpnext.regional.india.utils.set_place_of_supply']
}, },
('Sales Invoice', 'Purchase Invoice'): {
'validate': ['erpnext.regional.india.utils.validate_document_name']
},
"Contact": { "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue", "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
get_holidays_for_employee, create_additional_leave_ledger_entry get_holidays_for_employee, create_additional_leave_ledger_entry
@ -40,7 +40,12 @@ class CompensatoryLeaveRequest(Document):
def validate_holidays(self): def validate_holidays(self):
holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date) holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date)
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
frappe.throw(_("Compensatory leave request days not in valid holidays")) if date_diff(self.work_end_date, self.work_from_date):
msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
else:
msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date)))
frappe.throw(msg)
def on_submit(self): def on_submit(self):
company = frappe.db.get_value("Employee", self.employee, "company") company = frappe.db.get_value("Employee", self.employee, "company")
@ -63,7 +68,7 @@ class CompensatoryLeaveRequest(Document):
leave_allocation = self.create_leave_allocation(leave_period, date_difference) leave_allocation = self.create_leave_allocation(leave_period, date_difference)
self.leave_allocation=leave_allocation.name self.leave_allocation=leave_allocation.name
else: else:
frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date)))
def on_cancel(self): def on_cancel(self):
if self.leave_allocation: if self.leave_allocation:

View File

@ -80,6 +80,7 @@ class Employee(NestedSet):
self.update_user() self.update_user()
self.update_user_permissions() self.update_user_permissions()
self.reset_employee_emails_cache() self.reset_employee_emails_cache()
self.update_approver_role()
def update_user_permissions(self): def update_user_permissions(self):
if not self.create_user_permission: return if not self.create_user_permission: return
@ -145,6 +146,17 @@ class Employee(NestedSet):
user.save() user.save()
def update_approver_role(self):
if self.leave_approver:
user = frappe.get_doc("User", self.leave_approver)
user.flags.ignore_permissions = True
user.add_roles("Leave Approver")
if self.expense_approver:
user = frappe.get_doc("User", self.expense_approver)
user.flags.ignore_permissions = True
user.add_roles("Expense Approver")
def validate_date(self): def validate_date(self):
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
throw(_("Date of Birth cannot be greater than today.")) throw(_("Date of Birth cannot be greater than today."))
@ -503,7 +515,7 @@ def has_user_permission_for_employee(user_name, employee_name):
}) })
def has_upload_permission(doc, ptype='read', user=None): def has_upload_permission(doc, ptype='read', user=None):
if not user: if not user:
user = frappe.session.user user = frappe.session.user
if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
return True return True

View File

@ -181,7 +181,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)", "depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -201,7 +200,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-25 12:01:55.980721", "modified": "2021-03-31 14:42:47.321368",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Advance", "name": "Employee Advance",

View File

@ -6,7 +6,7 @@ import frappe, erpnext
from frappe import _ from frappe import _
from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.utils import get_fullname, flt, cstr, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, share_doc_with_approver
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@ -53,6 +53,9 @@ class ExpenseClaim(AccountsController):
elif self.docstatus == 1 and self.approval_status == 'Rejected': elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected' self.status = 'Rejected'
def on_update(self):
share_doc_with_approver(self, self.expense_approver)
def set_payable_account(self): def set_payable_account(self):
if not self.payable_account and not self.is_paid: if not self.payable_account and not self.is_paid:
self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account') self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account')
@ -211,6 +214,7 @@ class ExpenseClaim(AccountsController):
self.total_claimed_amount += flt(d.amount) self.total_claimed_amount += flt(d.amount)
self.total_sanctioned_amount += flt(d.sanctioned_amount) self.total_sanctioned_amount += flt(d.sanctioned_amount)
@frappe.whitelist()
def calculate_taxes(self): def calculate_taxes(self):
self.total_taxes_and_charges = 0 self.total_taxes_and_charges = 0
for tax in self.taxes: for tax in self.taxes:

View File

@ -95,12 +95,12 @@ class TestExpenseClaim(unittest.TestCase):
def test_rejected_expense_claim(self): def test_rejected_expense_claim(self):
payable_account = get_payable_account(company_name) payable_account = get_payable_account(company_name)
expense_claim = frappe.get_doc({ expense_claim = frappe.get_doc({
"doctype": "Expense Claim", "doctype": "Expense Claim",
"employee": "_T-Employee-00001", "employee": "_T-Employee-00001",
"payable_account": payable_account, "payable_account": payable_account,
"approval_status": "Rejected", "approval_status": "Rejected",
"expenses": "expenses":
[{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }] [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
}) })
expense_claim.submit() expense_claim.submit()
@ -110,6 +110,34 @@ class TestExpenseClaim(unittest.TestCase):
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name}) gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name})
self.assertEquals(len(gl_entry), 0) self.assertEquals(len(gl_entry), 0)
def test_expense_approver_perms(self):
user = "test_approver_perm_emp@example.com"
make_employee(user, "_Test Company")
# check doc shared
payable_account = get_payable_account("_Test Company")
expense_claim = make_expense_claim(payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
expense_claim.expense_approver = user
expense_claim.save()
self.assertTrue(expense_claim.name in frappe.share.get_shared("Expense Claim", user))
# check shared doc revoked
expense_claim.reload()
expense_claim.expense_approver = "test@example.com"
expense_claim.save()
self.assertTrue(expense_claim.name not in frappe.share.get_shared("Expense Claim", user))
expense_claim.reload()
expense_claim.expense_approver = user
expense_claim.save()
frappe.set_user(user)
expense_claim.reload()
expense_claim.status = "Approved"
expense_claim.submit()
frappe.set_user("Administrator")
def get_payable_account(company): def get_payable_account(company):
return frappe.get_cached_value('Company', company, 'default_payable_account') return frappe.get_cached_value('Company', company, 'default_payable_account')
@ -133,21 +161,21 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco
currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center'])
expense_claim = { expense_claim = {
"doctype": "Expense Claim", "doctype": "Expense Claim",
"employee": employee, "employee": employee,
"payable_account": payable_account, "payable_account": payable_account,
"approval_status": "Approved", "approval_status": "Approved",
"company": company, "company": company,
'currency': currency, "currency": currency,
"expenses": [{ "expenses": [{
"expense_type": "Travel", "expense_type": "Travel",
"default_account": account, "default_account": account,
"currency": currency, "currency": currency,
"amount": amount, "amount": amount,
"sanctioned_amount": sanctioned_amount, "sanctioned_amount": sanctioned_amount,
"cost_center": cost_center "cost_center": cost_center
}] }]
} }
if taxes: if taxes:
expense_claim.update(taxes) expense_claim.update(taxes)

View File

@ -99,6 +99,7 @@ class LeaveAllocation(Document):
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name), .format(formatdate(future_allocation[0].from_date), future_allocation[0].name),
BackDatedAllocationError) BackDatedAllocationError)
@frappe.whitelist()
def set_total_leaves_allocated(self): def set_total_leaves_allocated(self):
self.unused_leaves = get_carry_forwarded_leaves(self.employee, self.unused_leaves = get_carry_forwarded_leaves(self.employee,
self.leave_type, self.from_date, self.carry_forward) self.leave_type, self.from_date, self.carry_forward)

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
comma_or, get_fullname, add_days, nowdate, get_datetime_str comma_or, get_fullname, add_days, nowdate, get_datetime_str
from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
@ -43,6 +43,8 @@ class LeaveApplication(Document):
if frappe.db.get_single_value("HR Settings", "send_leave_notification"): if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
self.notify_leave_approver() self.notify_leave_approver()
share_doc_with_approver(self, self.leave_approver)
def on_submit(self): def on_submit(self):
if self.status == "Open": if self.status == "Open":
frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")) frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted"))
@ -417,6 +419,7 @@ class LeaveApplication(Document):
)) ))
create_leave_ledger_entry(self, args, submit) create_leave_ledger_entry(self, args, submit)
def get_allocation_expiry(employee, leave_type, to_date, from_date): def get_allocation_expiry(employee, leave_type, to_date, from_date):
''' Returns expiry of carry forward allocation in leave ledger entry ''' ''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry", expiry = frappe.get_all("Leave Ledger Entry",

View File

@ -11,6 +11,7 @@ from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
@ -56,6 +57,7 @@ class TestLeaveApplication(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
set_leave_approver() set_leave_approver()
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@ -230,8 +232,9 @@ class TestLeaveApplication(unittest.TestCase):
def test_optional_leave(self): def test_optional_leave(self):
leave_period = get_leave_period() leave_period = get_leave_period()
today = nowdate() today = nowdate()
from datetime import date
holiday_list = 'Test Holiday List for Optional Holiday' holiday_list = 'Test Holiday List for Optional Holiday'
optional_leave_date = add_days(today, 7)
if not frappe.db.exists('Holiday List', holiday_list): if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict( frappe.get_doc(dict(
doctype = 'Holiday List', doctype = 'Holiday List',
@ -239,7 +242,7 @@ class TestLeaveApplication(unittest.TestCase):
from_date = add_months(today, -6), from_date = add_months(today, -6),
to_date = add_months(today, 6), to_date = add_months(today, 6),
holidays = [ holidays = [
dict(holiday_date = today, description = 'Test') dict(holiday_date = optional_leave_date, description = 'Test')
] ]
)).insert() )).insert()
employee = get_employee() employee = get_employee()
@ -255,7 +258,7 @@ class TestLeaveApplication(unittest.TestCase):
allocate_leaves(employee, leave_period, leave_type, 10) allocate_leaves(employee, leave_period, leave_type, 10)
date = add_days(today, - 1) date = add_days(today, 6)
leave_application = frappe.get_doc(dict( leave_application = frappe.get_doc(dict(
doctype = 'Leave Application', doctype = 'Leave Application',
@ -270,14 +273,14 @@ class TestLeaveApplication(unittest.TestCase):
# can only apply on optional holidays # can only apply on optional holidays
self.assertRaises(NotAnOptionalHoliday, leave_application.insert) self.assertRaises(NotAnOptionalHoliday, leave_application.insert)
leave_application.from_date = today leave_application.from_date = optional_leave_date
leave_application.to_date = today leave_application.to_date = optional_leave_date
leave_application.status = "Approved" leave_application.status = "Approved"
leave_application.insert() leave_application.insert()
leave_application.submit() leave_application.submit()
# check leave balance is reduced # check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9) self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
def test_leaves_allowed(self): def test_leaves_allowed(self):
employee = get_employee() employee = get_employee()
@ -341,7 +344,7 @@ class TestLeaveApplication(unittest.TestCase):
to_date = add_days(date, 4), to_date = add_days(date, 4),
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
)) ))
self.assertRaises(frappe.ValidationError, leave_application.insert) self.assertRaises(frappe.ValidationError, leave_application.insert)
@ -363,7 +366,7 @@ class TestLeaveApplication(unittest.TestCase):
to_date = add_days(date, 4), to_date = add_days(date, 4),
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
)) ))
self.assertTrue(leave_application.insert()) self.assertTrue(leave_application.insert())
@ -393,7 +396,7 @@ class TestLeaveApplication(unittest.TestCase):
to_date = add_days(date, 4), to_date = add_days(date, 4),
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
)) ))
self.assertRaises(frappe.ValidationError, leave_application.insert) self.assertRaises(frappe.ValidationError, leave_application.insert)
@ -508,7 +511,7 @@ class TestLeaveApplication(unittest.TestCase):
description = "_Test Reason", description = "_Test Reason",
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
)) ))
leave_application.submit() leave_application.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name)) leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name))
@ -540,7 +543,7 @@ class TestLeaveApplication(unittest.TestCase):
description = "_Test Reason", description = "_Test Reason",
company = "_Test Company", company = "_Test Company",
docstatus = 1, docstatus = 1,
status = "Approved" status = "Approved"
)) ))
leave_application.submit() leave_application.submit()
@ -565,6 +568,48 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0) self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0)
def test_leave_approver_perms(self):
employee = get_employee()
user = "test_approver_perm_emp@example.com"
make_employee(user, "_Test Company")
# set approver for employee
employee.reload()
employee.leave_approver = user
employee.save()
self.assertTrue("Leave Approver" in frappe.get_roles(user))
make_allocation_record(employee.name)
application = self.get_application(_test_records[0])
application.from_date = '2018-01-01'
application.to_date = '2018-01-03'
application.leave_approver = user
application.insert()
self.assertTrue(application.name in frappe.share.get_shared("Leave Application", user))
# check shared doc revoked
application.reload()
application.leave_approver = "test@example.com"
application.save()
self.assertTrue(application.name not in frappe.share.get_shared("Leave Application", user))
application.reload()
application.leave_approver = user
application.save()
frappe.set_user(user)
application.reload()
application.status = "Approved"
application.submit()
# unset leave approver
frappe.set_user("Administrator")
employee.reload()
employee.leave_approver = ""
employee.save()
def create_carry_forwarded_allocation(employee, leave_type): def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation # initial leave allocation
leave_allocation = create_leave_allocation( leave_allocation = create_leave_allocation(

View File

@ -130,7 +130,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)", "depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -155,7 +154,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-25 11:56:06.777241", "modified": "2021-03-31 14:45:27.948207",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Encashment", "name": "Leave Encashment",

View File

@ -63,6 +63,7 @@ class LeaveEncashment(Document):
frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days) frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days)
self.create_leave_ledger_entry(submit=False) self.create_leave_ledger_entry(submit=False)
@frappe.whitelist()
def get_leave_details_for_encashment(self): def get_leave_details_for_encashment(self):
salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate())) salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate()))
if not salary_structure: if not salary_structure:

View File

@ -34,8 +34,8 @@ def validate_leave_allocation_against_leave_application(ledger):
""", (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date)) """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date))
if leave_application_records: if leave_application_records:
frappe.throw(_("Leave allocation %s is linked with leave application %s" frappe.throw(_("Leave allocation {0} is linked with the Leave Application {1}").format(
% (ledger.transaction_name, ', '.join(leave_application_records)))) ledger.transaction_name, ', '.join(leave_application_records)))
def create_leave_ledger_entry(ref_doc, args, submit=True): def create_leave_ledger_entry(ref_doc, args, submit=True):
ledger = frappe._dict( ledger = frappe._dict(
@ -52,7 +52,9 @@ def create_leave_ledger_entry(ref_doc, args, submit=True):
ledger.update(args) ledger.update(args)
if submit: if submit:
frappe.get_doc(ledger).submit() doc = frappe.get_doc(ledger)
doc.flags.ignore_permissions = 1
doc.submit()
else: else:
delete_ledger_entry(ledger) delete_ledger_entry(ledger)

View File

@ -36,6 +36,7 @@ class LeavePolicyAssignment(Document):
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
@frappe.whitelist()
def grant_leave_alloc_for_employee(self): def grant_leave_alloc_for_employee(self):
if self.leaves_allocated: if self.leaves_allocated:
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment")) frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))

View File

@ -7,6 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import formatdate, getdate from frappe.utils import formatdate, getdate
from erpnext.hr.utils import share_doc_with_approver
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
@ -17,6 +18,9 @@ class ShiftRequest(Document):
self.validate_approver() self.validate_approver()
self.validate_default_shift() self.validate_default_shift()
def on_update(self):
share_doc_with_approver(self, self.approver)
def on_submit(self): def on_submit(self):
if self.status not in ["Approved", "Rejected"]: if self.status not in ["Approved", "Rejected"]:
frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted")) frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted"))
@ -29,6 +33,7 @@ class ShiftRequest(Document):
if self.to_date: if self.to_date:
assignment_doc.end_date = self.to_date assignment_doc.end_date = self.to_date
assignment_doc.shift_request = self.name assignment_doc.shift_request = self.name
assignment_doc.flags.ignore_permissions = 1
assignment_doc.insert() assignment_doc.insert()
assignment_doc.submit() assignment_doc.submit()

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate, add_days from frappe.utils import nowdate, add_days
from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ["Shift Type"] test_dependencies = ["Shift Type"]
@ -19,19 +20,8 @@ class TestShiftRequest(unittest.TestCase):
set_shift_approver(department) set_shift_approver(department)
approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
shift_request = frappe.get_doc({ shift_request = make_shift_request(approver)
"doctype": "Shift Request",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"employee_name": "_Test Employee",
"from_date": nowdate(),
"to_date": add_days(nowdate(), 10),
"approver": approver,
"status": "Approved"
})
shift_request.insert()
shift_request.submit()
shift_assignments = frappe.db.sql(''' shift_assignments = frappe.db.sql('''
SELECT shift_request, employee SELECT shift_request, employee
FROM `tabShift Assignment` FROM `tabShift Assignment`
@ -44,8 +34,65 @@ class TestShiftRequest(unittest.TestCase):
shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')}) shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')})
self.assertEqual(shift_assignment_doc.docstatus, 2) self.assertEqual(shift_assignment_doc.docstatus, 2)
def test_shift_request_approver_perms(self):
employee = frappe.get_doc("Employee", "_T-Employee-00001")
user = "test_approver_perm_emp@example.com"
make_employee(user, "_Test Company")
# set approver for employee
employee.reload()
employee.shift_request_approver = user
employee.save()
shift_request = make_shift_request(user, do_not_submit=True)
self.assertTrue(shift_request.name in frappe.share.get_shared("Shift Request", user))
# check shared doc revoked
shift_request.reload()
department = frappe.get_value("Employee", "_T-Employee-00001", "department")
set_shift_approver(department)
department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
shift_request.approver = department_approver
shift_request.save()
self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user))
shift_request.reload()
shift_request.approver = user
shift_request.save()
frappe.set_user(user)
shift_request.reload()
shift_request.status = "Approved"
shift_request.submit()
# unset approver
frappe.set_user("Administrator")
employee.reload()
employee.shift_request_approver = ""
employee.save()
def set_shift_approver(department): def set_shift_approver(department):
department_doc = frappe.get_doc("Department", department) department_doc = frappe.get_doc("Department", department)
department_doc.append('shift_request_approver',{'approver': "test1@example.com"}) department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
department_doc.save() department_doc.save()
department_doc.reload() department_doc.reload()
def make_shift_request(approver, do_not_submit=0):
shift_request = frappe.get_doc({
"doctype": "Shift Request",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"employee_name": "_Test Employee",
"from_date": nowdate(),
"to_date": add_days(nowdate(), 10),
"approver": approver,
"status": "Approved"
}).insert()
if do_not_submit:
return shift_request
shift_request.submit()
return shift_request

View File

@ -15,6 +15,7 @@ from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class ShiftType(Document): class ShiftType(Document):
@frappe.whitelist()
def process_auto_attendance(self): def process_auto_attendance(self):
if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin: if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
return return

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