mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-09 08:11:19 +00:00
Merge remote-tracking branch 'upstream/version-13-hotfix' into fix-ecommerce-cart-badge
This commit is contained in:
@@ -255,11 +255,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
enable_check = "enable_deferred_revenue" \
|
enable_check = "enable_deferred_revenue" \
|
||||||
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
|
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
|
||||||
|
|
||||||
|
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
|
||||||
|
|
||||||
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
|
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
|
||||||
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
||||||
if not (start_date and end_date): return
|
if not (start_date and end_date): return
|
||||||
|
|
||||||
account_currency = get_account_currency(item.expense_account)
|
account_currency = get_account_currency(item.expense_account or item.income_account)
|
||||||
if doc.doctype == "Sales Invoice":
|
if doc.doctype == "Sales Invoice":
|
||||||
against, project = doc.customer, doc.project
|
against, project = doc.customer, doc.project
|
||||||
credit_account, debit_account = item.income_account, item.deferred_revenue_account
|
credit_account, debit_account = item.income_account, item.deferred_revenue_account
|
||||||
@@ -280,6 +282,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
if not amount:
|
if not amount:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# check if books nor frozen till endate:
|
||||||
|
if getdate(end_date) >= getdate(accounts_frozen_upto):
|
||||||
|
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||||
|
|
||||||
if via_journal_entry:
|
if via_journal_entry:
|
||||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||||
@@ -407,8 +413,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
|||||||
'account': credit_account,
|
'account': credit_account,
|
||||||
'credit': base_amount,
|
'credit': base_amount,
|
||||||
'credit_in_account_currency': amount,
|
'credit_in_account_currency': amount,
|
||||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
|
||||||
'party': against,
|
|
||||||
'account_currency': account_currency,
|
'account_currency': account_currency,
|
||||||
'reference_name': doc.name,
|
'reference_name': doc.name,
|
||||||
'reference_type': doc.doctype,
|
'reference_type': doc.doctype,
|
||||||
@@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
|||||||
'account': debit_account,
|
'account': debit_account,
|
||||||
'debit': base_amount,
|
'debit': base_amount,
|
||||||
'debit_in_account_currency': amount,
|
'debit_in_account_currency': amount,
|
||||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
|
||||||
'party': against,
|
|
||||||
'account_currency': account_currency,
|
'account_currency': account_currency,
|
||||||
'reference_name': doc.name,
|
'reference_name': doc.name,
|
||||||
'reference_type': doc.doctype,
|
'reference_type': doc.doctype,
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
|
|||||||
frm.trigger('add_toolbar_buttons');
|
frm.trigger('add_toolbar_buttons');
|
||||||
}
|
}
|
||||||
if (frm.has_perm('write')) {
|
if (frm.has_perm('write')) {
|
||||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
|
||||||
frm.trigger("update_account_number");
|
|
||||||
});
|
|
||||||
frm.add_custom_button(__('Merge Account'), function () {
|
frm.add_custom_button(__('Merge Account'), function () {
|
||||||
frm.trigger("merge_account");
|
frm.trigger("merge_account");
|
||||||
});
|
}, __('Actions'));
|
||||||
|
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||||
|
frm.trigger("update_account_number");
|
||||||
|
}, __('Actions'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
add_toolbar_buttons: function(frm) {
|
add_toolbar_buttons: function(frm) {
|
||||||
frm.add_custom_button(__('Chart of Accounts'),
|
frm.add_custom_button(__('Chart of Accounts'), () => {
|
||||||
function () { frappe.set_route("Tree", "Account"); });
|
frappe.set_route("Tree", "Account");
|
||||||
|
}, __('View'));
|
||||||
|
|
||||||
if (frm.doc.is_group == 1) {
|
if (frm.doc.is_group == 1) {
|
||||||
frm.add_custom_button(__('Group to Non-Group'), function () {
|
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: 'convert_group_to_ledger',
|
method: 'convert_group_to_ledger',
|
||||||
@@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
|
|||||||
frm.refresh();
|
frm.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, __('Actions'));
|
||||||
|
|
||||||
} else if (cint(frm.doc.is_group) == 0
|
} else if (cint(frm.doc.is_group) == 0
|
||||||
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
|
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
|
||||||
frm.add_custom_button(__('Ledger'), function () {
|
frm.add_custom_button(__('General Ledger'), function () {
|
||||||
frappe.route_options = {
|
frappe.route_options = {
|
||||||
"account": frm.doc.name,
|
"account": frm.doc.name,
|
||||||
"from_date": frappe.sys_defaults.year_start_date,
|
"from_date": frappe.sys_defaults.year_start_date,
|
||||||
@@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
|
|||||||
"company": frm.doc.company
|
"company": frm.doc.company
|
||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
});
|
}, __('View'));
|
||||||
|
|
||||||
frm.add_custom_button(__('Non-Group to Group'), function () {
|
frm.add_custom_button(__('Convert to Group'), function () {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: 'convert_ledger_to_group',
|
method: 'convert_ledger_to_group',
|
||||||
@@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
|
|||||||
frm.refresh();
|
frm.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, __('Actions'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
frm.set_query("bank_account", function () {
|
frm.set_query("bank_account", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
company: ["in", frm.doc.company],
|
company: frm.doc.company,
|
||||||
'is_company_account': 1
|
'is_company_account': 1
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
|
|||||||
"withdrawal",
|
"withdrawal",
|
||||||
"description",
|
"description",
|
||||||
"reference_number",
|
"reference_number",
|
||||||
"bank_account"
|
"bank_account",
|
||||||
|
"currency"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
|
|||||||
if(frm.doc.docstatus==1) {
|
if(frm.doc.docstatus==1) {
|
||||||
frm.add_custom_button(__('Reverse Journal Entry'), function() {
|
frm.add_custom_button(__('Reverse Journal Entry'), function() {
|
||||||
return erpnext.journal_entry.reverse_journal_entry(frm);
|
return erpnext.journal_entry.reverse_journal_entry(frm);
|
||||||
}, __('Make'));
|
}, __('Actions'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.__islocal) {
|
if (frm.doc.__islocal) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"voucher_type",
|
"voucher_type",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
|
"reversal_of",
|
||||||
"tax_withholding_category",
|
"tax_withholding_category",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"from_template",
|
"from_template",
|
||||||
@@ -515,13 +516,21 @@
|
|||||||
"fieldname": "apply_tds",
|
"fieldname": "apply_tds",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Apply Tax Withholding Amount "
|
"label": "Apply Tax Withholding Amount "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.docstatus",
|
||||||
|
"fieldname": "reversal_of",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Reversal Of",
|
||||||
|
"options": "Journal Entry",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 176,
|
"idx": 176,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-09 15:31:14.484029",
|
"modified": "2022-01-04 13:39:36.485954",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry",
|
"name": "Journal Entry",
|
||||||
|
|||||||
@@ -397,13 +397,14 @@ class JournalEntry(AccountsController):
|
|||||||
debit_or_credit = 'Debit' if d.debit else 'Credit'
|
debit_or_credit = 'Debit' if d.debit else 'Credit'
|
||||||
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
|
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
|
||||||
debit_or_credit)
|
debit_or_credit)
|
||||||
|
against_voucher = ['', against_voucher[1]]
|
||||||
else:
|
else:
|
||||||
if d.reference_type == "Sales Invoice":
|
if d.reference_type == "Sales Invoice":
|
||||||
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
||||||
else:
|
else:
|
||||||
party_account = against_voucher[1]
|
party_account = against_voucher[1]
|
||||||
|
|
||||||
if (against_voucher[0] != d.party or party_account != d.account):
|
if (against_voucher[0] != cstr(d.party) or party_account != d.account):
|
||||||
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
|
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
|
||||||
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
|
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
|
||||||
d.reference_type, d.reference_name))
|
d.reference_type, d.reference_name))
|
||||||
@@ -468,13 +469,22 @@ class JournalEntry(AccountsController):
|
|||||||
|
|
||||||
def set_against_account(self):
|
def set_against_account(self):
|
||||||
accounts_debited, accounts_credited = [], []
|
accounts_debited, accounts_credited = [], []
|
||||||
for d in self.get("accounts"):
|
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
|
||||||
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
|
for d in self.get('accounts'):
|
||||||
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
|
if d.reference_type == 'Sales Invoice':
|
||||||
|
field = 'customer'
|
||||||
|
else:
|
||||||
|
field = 'supplier'
|
||||||
|
|
||||||
for d in self.get("accounts"):
|
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
||||||
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
|
else:
|
||||||
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
|
for d in self.get("accounts"):
|
||||||
|
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
|
||||||
|
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
|
||||||
|
|
||||||
|
for d in self.get("accounts"):
|
||||||
|
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
|
||||||
|
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
|
||||||
|
|
||||||
def validate_debit_credit_amount(self):
|
def validate_debit_credit_amount(self):
|
||||||
for d in self.get('accounts'):
|
for d in self.get('accounts'):
|
||||||
@@ -1147,9 +1157,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
|||||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
def update_accounts(source, target, source_parent):
|
def post_process(source, target):
|
||||||
target.reference_type = "Journal Entry"
|
target.reversal_of = source.name
|
||||||
target.reference_name = source_parent.name
|
|
||||||
|
|
||||||
doclist = get_mapped_doc("Journal Entry", source_name, {
|
doclist = get_mapped_doc("Journal Entry", source_name, {
|
||||||
"Journal Entry": {
|
"Journal Entry": {
|
||||||
@@ -1167,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
|||||||
"debit": "credit",
|
"debit": "credit",
|
||||||
"credit_in_account_currency": "debit_in_account_currency",
|
"credit_in_account_currency": "debit_in_account_currency",
|
||||||
"credit": "debit",
|
"credit": "debit",
|
||||||
},
|
}
|
||||||
"postprocess": update_accounts,
|
|
||||||
},
|
},
|
||||||
}, target_doc)
|
}, target_doc, post_process)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"modified": "2019-07-25 14:57:33.187689",
|
"modified": "2022-01-04 15:25:06.053187",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool",
|
"name": "Opening Invoice Creation Tool",
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
frappe.scrub(row.party_type): row.party,
|
frappe.scrub(row.party_type): row.party,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||||
"update_stock": 0
|
"update_stock": 0,
|
||||||
|
"invoice_number": row.invoice_number
|
||||||
})
|
})
|
||||||
|
|
||||||
accounting_dimension = get_accounting_dimensions()
|
accounting_dimension = get_accounting_dimensions()
|
||||||
@@ -200,10 +201,13 @@ def start_import(invoices):
|
|||||||
names = []
|
names = []
|
||||||
for idx, d in enumerate(invoices):
|
for idx, d in enumerate(invoices):
|
||||||
try:
|
try:
|
||||||
|
invoice_number = None
|
||||||
|
if d.invoice_number:
|
||||||
|
invoice_number = d.invoice_number
|
||||||
publish(idx, len(invoices), d.doctype)
|
publish(idx, len(invoices), d.doctype)
|
||||||
doc = frappe.get_doc(d)
|
doc = frappe.get_doc(d)
|
||||||
doc.flags.ignore_mandatory = True
|
doc.flags.ignore_mandatory = True
|
||||||
doc.insert()
|
doc.insert(set_name=invoice_number)
|
||||||
doc.submit()
|
doc.submit()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
names.append(doc.name)
|
names.append(doc.name)
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
|||||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||||
make_company()
|
make_company()
|
||||||
|
|
||||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
|
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
|
||||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||||
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
||||||
party_1=party_1, party_2=party_2)
|
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
|
||||||
doc.update(args)
|
doc.update(args)
|
||||||
return doc.make_invoices()
|
return doc.make_invoices()
|
||||||
|
|
||||||
@@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
|||||||
# teardown
|
# teardown
|
||||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||||
|
|
||||||
|
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||||
|
company = "_Test Opening Invoice Company"
|
||||||
|
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||||
|
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
|
||||||
|
|
||||||
|
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
|
||||||
|
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
|
||||||
|
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||||
|
|
||||||
|
#teardown
|
||||||
|
for inv in [sales_inv1, sales_inv2]:
|
||||||
|
doc = frappe.get_doc('Sales Invoice', inv)
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
def get_opening_invoice_creation_dict(**args):
|
def get_opening_invoice_creation_dict(**args):
|
||||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||||
company = args.get("company", "_Test Company")
|
company = args.get("company", "_Test Company")
|
||||||
@@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
|
|||||||
"item_name": "Opening Item",
|
"item_name": "Opening Item",
|
||||||
"due_date": "2016-09-10",
|
"due_date": "2016-09-10",
|
||||||
"posting_date": "2016-09-05",
|
"posting_date": "2016-09-05",
|
||||||
"temporary_opening_account": get_temporary_opening_account(company)
|
"temporary_opening_account": get_temporary_opening_account(company),
|
||||||
|
"invoice_number": args.get("invoice_number")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"qty": 2.0,
|
"qty": 2.0,
|
||||||
@@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
|
|||||||
"item_name": "Opening Item",
|
"item_name": "Opening Item",
|
||||||
"due_date": "2016-09-10",
|
"due_date": "2016-09-10",
|
||||||
"posting_date": "2016-09-05",
|
"posting_date": "2016-09-05",
|
||||||
"temporary_opening_account": get_temporary_opening_account(company)
|
"temporary_opening_account": get_temporary_opening_account(company),
|
||||||
|
"invoice_number": None
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2017-08-29 04:26:36.159247",
|
"creation": "2017-08-29 04:26:36.159247",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"invoice_number",
|
||||||
"party_type",
|
"party_type",
|
||||||
"party",
|
"party",
|
||||||
"temporary_opening_account",
|
"temporary_opening_account",
|
||||||
@@ -114,7 +116,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-12-17 19:25:06.053187",
|
"modified": "2022-01-04 18:40:15.927675",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool Item",
|
"name": "Opening Invoice Creation Tool Item",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import ValidationError, _, scrub, throw
|
from frappe import ValidationError, _, scrub, throw
|
||||||
@@ -1524,6 +1525,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
|||||||
pe.received_amount = received_amount
|
pe.received_amount = received_amount
|
||||||
pe.letter_head = doc.get("letter_head")
|
pe.letter_head = doc.get("letter_head")
|
||||||
|
|
||||||
|
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
|
||||||
|
pe.project = (doc.get('project') or
|
||||||
|
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
|
||||||
|
|
||||||
if pe.party_type in ["Customer", "Supplier"]:
|
if pe.party_type in ["Customer", "Supplier"]:
|
||||||
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
||||||
pe.set("bank_account", bank_account)
|
pe.set("bank_account", bank_account)
|
||||||
@@ -1709,7 +1714,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
|||||||
|
|
||||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||||
total_discount = 0
|
total_discount = 0
|
||||||
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
|
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
|
||||||
|
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
|
||||||
|
|
||||||
|
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||||
for term in doc.payment_schedule:
|
for term in doc.payment_schedule:
|
||||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||||
if term.discount_type == 'Percentage':
|
if term.discount_type == 'Percentage':
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
|||||||
update_multi_mode_option,
|
update_multi_mode_option,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.party import get_due_date, get_party_account
|
from erpnext.accounts.party import get_due_date, get_party_account
|
||||||
|
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
|
||||||
|
|
||||||
|
|
||||||
@@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice):
|
|||||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||||
elif invalid_serial_nos:
|
elif invalid_serial_nos:
|
||||||
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
|
||||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||||
|
|
||||||
|
def validate_pos_reserved_batch_qty(self, item):
|
||||||
|
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
|
||||||
|
|
||||||
|
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
|
||||||
|
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
|
||||||
|
|
||||||
|
bold_item_name = frappe.bold(item.item_name)
|
||||||
|
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
|
||||||
|
bold_invalid_batch_no = frappe.bold(item.batch_no)
|
||||||
|
|
||||||
|
if (available_batch_qty - reserved_batch_qty) == 0:
|
||||||
|
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
|
||||||
|
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
|
||||||
|
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
|
||||||
|
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
|
||||||
|
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
|
||||||
|
|
||||||
def validate_delivered_serial_nos(self, item):
|
def validate_delivered_serial_nos(self, item):
|
||||||
serial_nos = get_serial_nos(item.serial_no)
|
serial_nos = get_serial_nos(item.serial_no)
|
||||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||||
@@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice):
|
|||||||
if d.serial_no:
|
if d.serial_no:
|
||||||
self.validate_pos_reserved_serial_nos(d)
|
self.validate_pos_reserved_serial_nos(d)
|
||||||
self.validate_delivered_serial_nos(d)
|
self.validate_delivered_serial_nos(d)
|
||||||
|
elif d.batch_no:
|
||||||
|
self.validate_pos_reserved_batch_qty(d)
|
||||||
else:
|
else:
|
||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
return
|
||||||
@@ -334,7 +354,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
if not for_validate and not self.customer:
|
if not for_validate and not self.customer:
|
||||||
self.customer = profile.customer
|
self.customer = profile.customer
|
||||||
|
|
||||||
self.ignore_pricing_rule = profile.ignore_pricing_rule
|
|
||||||
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
||||||
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||||
|
|
||||||
|
|||||||
@@ -521,6 +521,72 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||||
self.assertEqual(rounded_total, 400)
|
self.assertEqual(rounded_total, 400)
|
||||||
|
|
||||||
|
def test_pos_batch_item_qty_validation(self):
|
||||||
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
|
create_batch_item_with_batch,
|
||||||
|
)
|
||||||
|
create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
|
||||||
|
item = frappe.get_doc('Item', '_BATCH ITEM')
|
||||||
|
batch = frappe.get_doc('Batch', 'TestBatch 01')
|
||||||
|
batch.submit()
|
||||||
|
item.batch_no = 'TestBatch 01'
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
|
||||||
|
|
||||||
|
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
|
||||||
|
pos_inv1.items[0].batch_no = 'TestBatch 01'
|
||||||
|
pos_inv1.save()
|
||||||
|
pos_inv1.submit()
|
||||||
|
|
||||||
|
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
|
||||||
|
pos_inv2.items[0].batch_no = 'TestBatch 01'
|
||||||
|
pos_inv2.save()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
|
||||||
|
|
||||||
|
#teardown
|
||||||
|
pos_inv1.reload()
|
||||||
|
pos_inv1.cancel()
|
||||||
|
pos_inv1.delete()
|
||||||
|
pos_inv2.reload()
|
||||||
|
pos_inv2.delete()
|
||||||
|
se.cancel()
|
||||||
|
batch.reload()
|
||||||
|
batch.cancel()
|
||||||
|
batch.delete()
|
||||||
|
|
||||||
|
def test_ignore_pricing_rule(self):
|
||||||
|
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
|
||||||
|
|
||||||
|
item_price = frappe.get_doc({
|
||||||
|
'doctype': 'Item Price',
|
||||||
|
'item_code': '_Test Item',
|
||||||
|
'price_list': '_Test Price List',
|
||||||
|
'price_list_rate': '450',
|
||||||
|
})
|
||||||
|
item_price.insert()
|
||||||
|
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
|
||||||
|
pr.save()
|
||||||
|
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||||
|
pos_inv.items[0].rate = 300
|
||||||
|
pos_inv.save()
|
||||||
|
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||||
|
# rate shouldn't change
|
||||||
|
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||||
|
|
||||||
|
pos_inv.ignore_pricing_rule = 1
|
||||||
|
pos_inv.items[0].rate = 300
|
||||||
|
pos_inv.save()
|
||||||
|
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||||
|
# rate should change since pricing rules are ignored
|
||||||
|
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||||
|
|
||||||
|
item_price.delete()
|
||||||
|
pos_inv.delete()
|
||||||
|
pr.delete()
|
||||||
|
|
||||||
|
|
||||||
def create_pos_invoice(**args):
|
def create_pos_invoice(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
pos_profile = None
|
pos_profile = None
|
||||||
@@ -557,7 +623,8 @@ def create_pos_invoice(**args):
|
|||||||
"income_account": args.income_account or "Sales - _TC",
|
"income_account": args.income_account or "Sales - _TC",
|
||||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||||
"serial_no": args.serial_no
|
"serial_no": args.serial_no,
|
||||||
|
"batch_no": args.batch_no
|
||||||
})
|
})
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
@@ -570,3 +637,8 @@ def create_pos_invoice(**args):
|
|||||||
pos_inv.payment_schedule = []
|
pos_inv.payment_schedule = []
|
||||||
|
|
||||||
return pos_inv
|
return pos_inv
|
||||||
|
|
||||||
|
def make_batch_item(item_name):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
if not frappe.db.exists(item_name):
|
||||||
|
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))
|
||||||
@@ -652,7 +652,7 @@ def make_pricing_rule(**args):
|
|||||||
"rate": args.rate or 0.0,
|
"rate": args.rate or 0.0,
|
||||||
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
||||||
"condition": args.condition or '',
|
"condition": args.condition or '',
|
||||||
"priority": 1,
|
"priority": args.priority or 1,
|
||||||
"discount_amount": args.discount_amount or 0.0,
|
"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
|
||||||
})
|
})
|
||||||
@@ -678,6 +678,8 @@ def make_pricing_rule(**args):
|
|||||||
if args.get(applicable_for):
|
if args.get(applicable_for):
|
||||||
doc.db_set(applicable_for, args.get(applicable_for))
|
doc.db_set(applicable_for, args.get(applicable_for))
|
||||||
|
|
||||||
|
return doc
|
||||||
|
|
||||||
def setup_pricing_rule_data():
|
def setup_pricing_rule_data():
|
||||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
|
|||||||
@@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# Checked both rounding_adjustment and rounded_total
|
# Checked both rounding_adjustment and rounded_total
|
||||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||||
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||||
|
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||||
|
else self.base_grand_total, self.precision("base_grand_total"))
|
||||||
|
|
||||||
if grand_total and not self.is_internal_transfer():
|
if grand_total and not self.is_internal_transfer():
|
||||||
# Did not use base_grand_total to book rounding loss gle
|
# Did not use base_grand_total to book rounding loss gle
|
||||||
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
|
|
||||||
self.precision("grand_total"))
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict({
|
self.get_gl_dict({
|
||||||
"account": self.credit_to,
|
"account": self.credit_to,
|
||||||
@@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"party": self.supplier,
|
"party": self.supplier,
|
||||||
"due_date": self.due_date,
|
"due_date": self.due_date,
|
||||||
"against": self.against_expense_account,
|
"against": self.against_expense_account,
|
||||||
"credit": grand_total_in_company_currency,
|
"credit": base_grand_total,
|
||||||
"credit_in_account_currency": grand_total_in_company_currency \
|
"credit_in_account_currency": base_grand_total \
|
||||||
if self.party_account_currency==self.company_currency else grand_total,
|
if self.party_account_currency==self.company_currency else grand_total,
|
||||||
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
|
|||||||
@@ -1213,7 +1213,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
|||||||
def update_tax_witholding_category(company, account):
|
def update_tax_witholding_category(company, account):
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021')
|
fiscal_year = get_fiscal_year(date=nowdate())
|
||||||
|
|
||||||
if not frappe.db.get_value('Tax Withholding Rate',
|
if not frappe.db.get_value('Tax Withholding Rate',
|
||||||
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
||||||
|
|||||||
@@ -651,7 +651,7 @@
|
|||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Ignore Pricing Rule",
|
"label": "Ignore Pricing Rule",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 1,
|
"permlevel": 0,
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2038,7 +2038,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-10-21 20:19:38.667508",
|
"modified": "2021-12-23 20:19:38.667508",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -879,11 +879,11 @@ class SalesInvoice(SellingController):
|
|||||||
# Checked both rounding_adjustment and rounded_total
|
# Checked both rounding_adjustment and rounded_total
|
||||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||||
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||||
|
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||||
|
else self.base_grand_total, self.precision("base_grand_total"))
|
||||||
|
|
||||||
if grand_total and not self.is_internal_transfer():
|
if grand_total and not self.is_internal_transfer():
|
||||||
# Didnot use base_grand_total to book rounding loss gle
|
# Didnot use base_grand_total to book rounding loss gle
|
||||||
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
|
|
||||||
self.precision("grand_total"))
|
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict({
|
self.get_gl_dict({
|
||||||
"account": self.debit_to,
|
"account": self.debit_to,
|
||||||
@@ -891,8 +891,8 @@ class SalesInvoice(SellingController):
|
|||||||
"party": self.customer,
|
"party": self.customer,
|
||||||
"due_date": self.due_date,
|
"due_date": self.due_date,
|
||||||
"against": self.against_income_account,
|
"against": self.against_income_account,
|
||||||
"debit": grand_total_in_company_currency,
|
"debit": base_grand_total,
|
||||||
"debit_in_account_currency": grand_total_in_company_currency \
|
"debit_in_account_currency": base_grand_total \
|
||||||
if self.party_account_currency==self.company_currency else grand_total,
|
if self.party_account_currency==self.company_currency else grand_total,
|
||||||
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
|
|||||||
from erpnext.accounts.utils import PaymentEntryUnlinkError
|
from erpnext.accounts.utils import PaymentEntryUnlinkError
|
||||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||||
|
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||||
from erpnext.regional.india.utils import get_ewb_data
|
from erpnext.regional.india.utils import get_ewb_data
|
||||||
@@ -1788,47 +1789,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||||
|
|
||||||
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
|
||||||
|
|
||||||
deferred_account = create_account(account_name="Deferred Revenue",
|
|
||||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
|
||||||
|
|
||||||
item = create_item("_Test Item for Deferred Accounting")
|
|
||||||
item.enable_deferred_revenue = 1
|
|
||||||
item.deferred_revenue_account = deferred_account
|
|
||||||
item.no_of_months = 12
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
|
|
||||||
si.items[0].enable_deferred_revenue = 1
|
|
||||||
si.items[0].service_start_date = "2019-01-10"
|
|
||||||
si.items[0].service_end_date = "2019-03-15"
|
|
||||||
si.items[0].deferred_revenue_account = deferred_account
|
|
||||||
si.save()
|
|
||||||
si.submit()
|
|
||||||
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
|
|
||||||
|
|
||||||
pda1 = frappe.get_doc(dict(
|
|
||||||
doctype='Process Deferred Accounting',
|
|
||||||
posting_date=nowdate(),
|
|
||||||
start_date="2019-01-01",
|
|
||||||
end_date="2019-03-31",
|
|
||||||
type="Income",
|
|
||||||
company="_Test Company"
|
|
||||||
))
|
|
||||||
|
|
||||||
pda1.insert()
|
|
||||||
self.assertRaises(frappe.ValidationError, pda1.submit)
|
|
||||||
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
|
||||||
|
|
||||||
def test_fixed_deferred_revenue(self):
|
def test_fixed_deferred_revenue(self):
|
||||||
deferred_account = create_account(account_name="Deferred Revenue",
|
deferred_account = create_account(account_name="Deferred Revenue",
|
||||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||||
@@ -2358,6 +2318,41 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.reload()
|
si.reload()
|
||||||
self.assertEqual(si.status, "Paid")
|
self.assertEqual(si.status, "Paid")
|
||||||
|
|
||||||
|
def test_update_invoice_status(self):
|
||||||
|
today = nowdate()
|
||||||
|
|
||||||
|
# Sales Invoice without Payment Schedule
|
||||||
|
si = create_sales_invoice(posting_date=add_days(today, -5))
|
||||||
|
|
||||||
|
# Sales Invoice with Payment Schedule
|
||||||
|
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
|
||||||
|
si_with_payment_schedule.extend("payment_schedule", [
|
||||||
|
{
|
||||||
|
"due_date": add_days(today, -5),
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"payment_amount": si_with_payment_schedule.grand_total / 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"due_date": add_days(today, 5),
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"payment_amount": si_with_payment_schedule.grand_total / 2
|
||||||
|
}
|
||||||
|
])
|
||||||
|
si_with_payment_schedule.submit()
|
||||||
|
|
||||||
|
|
||||||
|
for invoice in (si, si_with_payment_schedule):
|
||||||
|
invoice.db_set("status", "Unpaid")
|
||||||
|
update_invoice_status()
|
||||||
|
invoice.reload()
|
||||||
|
self.assertEqual(invoice.status, "Overdue")
|
||||||
|
|
||||||
|
invoice.db_set("status", "Unpaid and Discounted")
|
||||||
|
update_invoice_status()
|
||||||
|
invoice.reload()
|
||||||
|
self.assertEqual(invoice.status, "Overdue and Discounted")
|
||||||
|
|
||||||
|
|
||||||
def test_sales_commission(self):
|
def test_sales_commission(self):
|
||||||
si = frappe.copy_doc(test_records[0])
|
si = frappe.copy_doc(test_records[0])
|
||||||
item = copy.deepcopy(si.get('items')[0])
|
item = copy.deepcopy(si.get('items')[0])
|
||||||
@@ -2419,6 +2414,74 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
|
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
|
||||||
|
|
||||||
|
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||||
|
deferred_account = create_account(account_name="Deferred Revenue",
|
||||||
|
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||||
|
|
||||||
|
acc_settings = frappe.get_single('Accounts Settings')
|
||||||
|
acc_settings.book_deferred_entries_via_journal_entry = 1
|
||||||
|
acc_settings.submit_journal_entries = 1
|
||||||
|
acc_settings.save()
|
||||||
|
|
||||||
|
item = create_item("_Test Item for Deferred Accounting")
|
||||||
|
item.enable_deferred_expense = 1
|
||||||
|
item.deferred_revenue_account = deferred_account
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
|
||||||
|
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
|
||||||
|
|
||||||
|
si.set_posting_time = 1
|
||||||
|
si.posting_date = '2019-01-01'
|
||||||
|
si.debit_to = '_Test Receivable USD - _TC'
|
||||||
|
si.items[0].enable_deferred_revenue = 1
|
||||||
|
si.items[0].service_start_date = "2019-01-01"
|
||||||
|
si.items[0].service_end_date = "2019-03-30"
|
||||||
|
si.items[0].deferred_expense_account = deferred_account
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||||
|
|
||||||
|
pda1 = frappe.get_doc(dict(
|
||||||
|
doctype='Process Deferred Accounting',
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2019-01-01",
|
||||||
|
end_date="2019-03-31",
|
||||||
|
type="Income",
|
||||||
|
company="_Test Company"
|
||||||
|
))
|
||||||
|
|
||||||
|
pda1.insert()
|
||||||
|
pda1.submit()
|
||||||
|
|
||||||
|
expected_gle = [
|
||||||
|
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
|
||||||
|
[deferred_account, 2089.89, 0.0, "2019-01-28"],
|
||||||
|
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
|
||||||
|
[deferred_account, 1887.64, 0.0, "2019-02-28"],
|
||||||
|
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
|
||||||
|
[deferred_account, 2022.47, 0.0, "2019-03-15"]
|
||||||
|
]
|
||||||
|
|
||||||
|
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||||
|
from `tabGL Entry`
|
||||||
|
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
|
||||||
|
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
|
||||||
|
|
||||||
|
for i, gle in enumerate(gl_entries):
|
||||||
|
self.assertEqual(expected_gle[i][0], gle.account)
|
||||||
|
self.assertEqual(expected_gle[i][1], gle.credit)
|
||||||
|
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||||
|
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||||
|
|
||||||
|
acc_settings = frappe.get_single('Accounts Settings')
|
||||||
|
acc_settings.book_deferred_entries_via_journal_entry = 0
|
||||||
|
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||||
|
acc_settings.save()
|
||||||
|
|
||||||
|
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||||
|
|
||||||
def get_sales_invoice_for_e_invoice():
|
def get_sales_invoice_for_e_invoice():
|
||||||
si = make_sales_invoice_for_ewaybill()
|
si = make_sales_invoice_for_ewaybill()
|
||||||
si.naming_series = 'INV-2020-.#####'
|
si.naming_series = 'INV-2020-.#####'
|
||||||
|
|||||||
@@ -28,14 +28,14 @@
|
|||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "single_threshold",
|
"fieldname": "single_threshold",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Single Transaction Threshold"
|
"label": "Single Transaction Threshold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 3,
|
"columns": 3,
|
||||||
"fieldname": "cumulative_threshold",
|
"fieldname": "cumulative_threshold",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Cumulative Transaction Threshold"
|
"label": "Cumulative Transaction Threshold"
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-31 11:42:12.213977",
|
"modified": "2022-01-13 12:04:42.904263",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withholding Rate",
|
"name": "Tax Withholding Rate",
|
||||||
@@ -68,5 +68,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
|||||||
"label": __("Show Future Payments"),
|
"label": __("Show Future Payments"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"show_gl_balance",
|
||||||
|
"label": __("Show GL Balance"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
onload: function(report) {
|
onload: function(report) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint, flt
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
||||||
@@ -37,6 +37,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
|
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
|
||||||
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
|
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
|
||||||
|
|
||||||
|
if self.filters.show_gl_balance:
|
||||||
|
gl_balance_map = get_gl_balance(self.filters.report_date)
|
||||||
|
|
||||||
for party, party_dict in iteritems(self.party_total):
|
for party, party_dict in iteritems(self.party_total):
|
||||||
if party_dict.outstanding == 0:
|
if party_dict.outstanding == 0:
|
||||||
continue
|
continue
|
||||||
@@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
# but in summary report advance shown in separate column
|
# but in summary report advance shown in separate column
|
||||||
row.paid -= row.advance
|
row.paid -= row.advance
|
||||||
|
|
||||||
|
if self.filters.show_gl_balance:
|
||||||
|
row.gl_balance = gl_balance_map.get(party)
|
||||||
|
row.diff = flt(row.outstanding) - flt(row.gl_balance)
|
||||||
|
|
||||||
self.data.append(row)
|
self.data.append(row)
|
||||||
|
|
||||||
def get_party_total(self, args):
|
def get_party_total(self, args):
|
||||||
@@ -115,6 +122,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
self.add_column(_(credit_debit_label), fieldname='credit_note')
|
self.add_column(_(credit_debit_label), fieldname='credit_note')
|
||||||
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
|
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
|
||||||
|
|
||||||
|
if self.filters.show_gl_balance:
|
||||||
|
self.add_column(_('GL Balance'), fieldname='gl_balance')
|
||||||
|
self.add_column(_('Difference'), fieldname='diff')
|
||||||
|
|
||||||
self.setup_ageing_columns()
|
self.setup_ageing_columns()
|
||||||
|
|
||||||
if self.party_type == "Customer":
|
if self.party_type == "Customer":
|
||||||
@@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
# Add column for total due amount
|
# Add column for total due amount
|
||||||
self.add_column(label="Total Amount Due", fieldname='total_due')
|
self.add_column(label="Total Amount Due", fieldname='total_due')
|
||||||
|
|
||||||
|
def get_gl_balance(report_date):
|
||||||
|
return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
|
||||||
|
filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))
|
||||||
|
|||||||
@@ -121,20 +121,21 @@ class Deferred_Item(object):
|
|||||||
"""
|
"""
|
||||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||||
"""
|
"""
|
||||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
if self.service_start_date != self.service_end_date:
|
||||||
self.estimate_for_period_list = get_period_list(
|
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||||
self.filters.from_fiscal_year,
|
self.estimate_for_period_list = get_period_list(
|
||||||
self.filters.to_fiscal_year,
|
self.filters.from_fiscal_year,
|
||||||
add_days(self.last_entry_date, 1),
|
self.filters.to_fiscal_year,
|
||||||
self.period_list[-1].to_date,
|
add_days(self.last_entry_date, 1),
|
||||||
"Date Range",
|
self.period_list[-1].to_date,
|
||||||
"Monthly",
|
"Date Range",
|
||||||
company=self.filters.company,
|
"Monthly",
|
||||||
)
|
company=self.filters.company,
|
||||||
for period in self.estimate_for_period_list:
|
)
|
||||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
for period in self.estimate_for_period_list:
|
||||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||||
self.gle_entries.append(gle)
|
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||||
|
self.gle_entries.append(gle)
|
||||||
|
|
||||||
def calculate_item_revenue_expense_for_period(self):
|
def calculate_item_revenue_expense_for_period(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
"fieldname": "include_dimensions",
|
"fieldname": "include_dimensions",
|
||||||
"label": __("Consider Accounting Dimensions"),
|
"label": __("Consider Accounting Dimensions"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"default": 0
|
"default": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "show_opening_entries",
|
"fieldname": "show_opening_entries",
|
||||||
|
|||||||
@@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
|
|
||||||
elif group_by_voucher_consolidated:
|
elif group_by_voucher_consolidated:
|
||||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||||
for dim in accounting_dimensions:
|
if filters.get("include_dimensions"):
|
||||||
keylist.append(gle.get(dim))
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get("cost_center"))
|
keylist.append(gle.get(dim))
|
||||||
|
keylist.append(gle.get("cost_center"))
|
||||||
|
|
||||||
key = tuple(keylist)
|
key = tuple(keylist)
|
||||||
if key not in consolidated_gle:
|
if key not in consolidated_gle:
|
||||||
consolidated_gle.setdefault(key, gle)
|
consolidated_gle.setdefault(key, gle)
|
||||||
@@ -595,14 +597,14 @@ def get_columns(filters):
|
|||||||
"fieldname": dim.fieldname,
|
"fieldname": dim.fieldname,
|
||||||
"width": 100
|
"width": 100
|
||||||
})
|
})
|
||||||
|
columns.append({
|
||||||
columns.extend([
|
|
||||||
{
|
|
||||||
"label": _("Cost Center"),
|
"label": _("Cost Center"),
|
||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
"width": 100
|
"width": 100
|
||||||
},
|
})
|
||||||
|
|
||||||
|
columns.extend([
|
||||||
{
|
{
|
||||||
"label": _("Against Voucher Type"),
|
"label": _("Against Voucher Type"),
|
||||||
"fieldname": "against_voucher_type",
|
"fieldname": "against_voucher_type",
|
||||||
|
|||||||
@@ -583,7 +583,17 @@ class Asset(AccountsController):
|
|||||||
return purchase_document
|
return purchase_document
|
||||||
|
|
||||||
def get_fixed_asset_account(self):
|
def get_fixed_asset_account(self):
|
||||||
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
||||||
|
if not fixed_asset_account:
|
||||||
|
frappe.throw(
|
||||||
|
_("Set {0} in asset category {1} for company {2}").format(
|
||||||
|
frappe.bold("Fixed Asset Account"),
|
||||||
|
frappe.bold(self.asset_category),
|
||||||
|
frappe.bold(self.company),
|
||||||
|
),
|
||||||
|
title=_("Account not Found"),
|
||||||
|
)
|
||||||
|
return fixed_asset_account
|
||||||
|
|
||||||
def get_cwip_account(self, cwip_enabled=False):
|
def get_cwip_account(self, cwip_enabled=False):
|
||||||
cwip_account = None
|
cwip_account = None
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
@@ -113,7 +114,7 @@ class AccountsController(TransactionBase):
|
|||||||
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
|
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if not self.get('is_return'):
|
if not self.get('is_return') and not self.get('is_debit_note'):
|
||||||
self.validate_qty_is_not_zero()
|
self.validate_qty_is_not_zero()
|
||||||
|
|
||||||
if self.get("_action") and self._action != "update_after_submit":
|
if self.get("_action") and self._action != "update_after_submit":
|
||||||
@@ -190,8 +191,6 @@ class AccountsController(TransactionBase):
|
|||||||
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
|
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
|
||||||
elif getdate(self.posting_date) > getdate(d.service_end_date):
|
elif getdate(self.posting_date) > getdate(d.service_end_date):
|
||||||
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
|
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
|
||||||
elif getdate(self.posting_date) > getdate(d.service_start_date):
|
|
||||||
frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx))
|
|
||||||
|
|
||||||
def validate_invoice_documents_schedule(self):
|
def validate_invoice_documents_schedule(self):
|
||||||
self.validate_payment_schedule_dates()
|
self.validate_payment_schedule_dates()
|
||||||
@@ -1692,58 +1691,69 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
|||||||
def update_invoice_status():
|
def update_invoice_status():
|
||||||
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||||
today = getdate()
|
today = getdate()
|
||||||
|
payment_schedule = frappe.qb.DocType("Payment Schedule")
|
||||||
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||||
frappe.db.sql("""
|
invoice = frappe.qb.DocType(doctype)
|
||||||
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
|
|
||||||
WHERE invoice.docstatus = 1
|
consider_base_amount = invoice.party_account_currency != invoice.currency
|
||||||
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
|
payment_amount = (
|
||||||
AND invoice.outstanding_amount > 0
|
frappe.qb.terms.Case()
|
||||||
AND (
|
.when(consider_base_amount, payment_schedule.base_payment_amount)
|
||||||
{or_condition}
|
.else_(payment_schedule.payment_amount)
|
||||||
(
|
|
||||||
(
|
|
||||||
CASE
|
|
||||||
WHEN invoice.party_account_currency = invoice.currency
|
|
||||||
THEN (
|
|
||||||
CASE
|
|
||||||
WHEN invoice.disable_rounded_total
|
|
||||||
THEN invoice.grand_total
|
|
||||||
ELSE invoice.rounded_total
|
|
||||||
END
|
|
||||||
)
|
|
||||||
ELSE (
|
|
||||||
CASE
|
|
||||||
WHEN invoice.disable_rounded_total
|
|
||||||
THEN invoice.base_grand_total
|
|
||||||
ELSE invoice.base_rounded_total
|
|
||||||
END
|
|
||||||
)
|
|
||||||
END
|
|
||||||
) - invoice.outstanding_amount
|
|
||||||
) < (
|
|
||||||
SELECT SUM(
|
|
||||||
CASE
|
|
||||||
WHEN invoice.party_account_currency = invoice.currency
|
|
||||||
THEN ps.payment_amount
|
|
||||||
ELSE ps.base_payment_amount
|
|
||||||
END
|
|
||||||
)
|
|
||||||
FROM `tabPayment Schedule` ps
|
|
||||||
WHERE ps.parent = invoice.name
|
|
||||||
AND ps.due_date < %(today)s
|
|
||||||
)
|
|
||||||
)
|
|
||||||
""".format(
|
|
||||||
doctype=doctype,
|
|
||||||
or_condition=(
|
|
||||||
"invoice.is_pos AND invoice.due_date < %(today)s OR"
|
|
||||||
if doctype == "Sales Invoice"
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
), {"today": today}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
payable_amount = (
|
||||||
|
frappe.qb.from_(payment_schedule)
|
||||||
|
.select(Sum(payment_amount))
|
||||||
|
.where(
|
||||||
|
(payment_schedule.parent == invoice.name)
|
||||||
|
& (payment_schedule.due_date < today)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = (
|
||||||
|
frappe.qb.terms.Case()
|
||||||
|
.when(invoice.disable_rounded_total, invoice.grand_total)
|
||||||
|
.else_(invoice.rounded_total)
|
||||||
|
)
|
||||||
|
|
||||||
|
base_total = (
|
||||||
|
frappe.qb.terms.Case()
|
||||||
|
.when(invoice.disable_rounded_total, invoice.base_grand_total)
|
||||||
|
.else_(invoice.base_rounded_total)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_amount = (
|
||||||
|
frappe.qb.terms.Case()
|
||||||
|
.when(consider_base_amount, base_total)
|
||||||
|
.else_(total)
|
||||||
|
)
|
||||||
|
|
||||||
|
is_overdue = total_amount - invoice.outstanding_amount < payable_amount
|
||||||
|
|
||||||
|
conditions = (
|
||||||
|
(invoice.docstatus == 1)
|
||||||
|
& (invoice.outstanding_amount > 0)
|
||||||
|
& (
|
||||||
|
invoice.status.like("Unpaid%")
|
||||||
|
| invoice.status.like("Partly Paid%")
|
||||||
|
)
|
||||||
|
& (
|
||||||
|
((invoice.is_pos & invoice.due_date < today) | is_overdue)
|
||||||
|
if doctype == "Sales Invoice"
|
||||||
|
else is_overdue
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
status = (
|
||||||
|
frappe.qb.terms.Case()
|
||||||
|
.when(invoice.status.like("%Discounted"), "Overdue and Discounted")
|
||||||
|
.else_("Overdue")
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.qb.update(invoice).set("status", status).where(conditions).run()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||||
if not terms_template:
|
if not terms_template:
|
||||||
@@ -2113,6 +2123,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.update_status_updater()
|
parent.update_status_updater()
|
||||||
else:
|
else:
|
||||||
parent.check_credit_limit()
|
parent.check_credit_limit()
|
||||||
|
|
||||||
|
# reset index of child table
|
||||||
|
for idx, row in enumerate(parent.get(child_docname), start=1):
|
||||||
|
row.idx = idx
|
||||||
|
|
||||||
parent.save()
|
parent.save()
|
||||||
|
|
||||||
if parent_doctype == 'Purchase Order':
|
if parent_doctype == 'Purchase Order':
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ class SellingController(StockController):
|
|||||||
# Get incoming rate based on original item cost based on valuation method
|
# Get incoming rate based on original item cost based on valuation method
|
||||||
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
|
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
|
||||||
|
|
||||||
if not d.incoming_rate:
|
if not (self.get("is_return") and d.incoming_rate):
|
||||||
d.incoming_rate = get_incoming_rate({
|
d.incoming_rate = get_incoming_rate({
|
||||||
"item_code": d.item_code,
|
"item_code": d.item_code,
|
||||||
"warehouse": d.warehouse,
|
"warehouse": d.warehouse,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import (
|
|||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
|
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||||
|
|
||||||
|
|
||||||
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
||||||
@@ -111,17 +111,6 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
self.check_expense_account(item_row)
|
self.check_expense_account(item_row)
|
||||||
|
|
||||||
# If the item does not have the allow zero valuation rate flag set
|
|
||||||
# and ( valuation rate not mentioned in an incoming entry
|
|
||||||
# or incoming entry not found while delivering the item),
|
|
||||||
# try to pick valuation rate from previous sle or Item master and update in SLE
|
|
||||||
# Otherwise, throw an exception
|
|
||||||
|
|
||||||
if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \
|
|
||||||
and not item_row.get("allow_zero_valuation_rate"):
|
|
||||||
|
|
||||||
sle = self.update_stock_ledger_entries(sle)
|
|
||||||
|
|
||||||
# expense account/ target_warehouse / source_warehouse
|
# expense account/ target_warehouse / source_warehouse
|
||||||
if item_row.get('target_warehouse'):
|
if item_row.get('target_warehouse'):
|
||||||
warehouse = item_row.get('target_warehouse')
|
warehouse = item_row.get('target_warehouse')
|
||||||
@@ -164,26 +153,6 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
return frappe.flags.debit_field_precision
|
return frappe.flags.debit_field_precision
|
||||||
|
|
||||||
def update_stock_ledger_entries(self, sle):
|
|
||||||
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
|
||||||
self.doctype, self.name, currency=self.company_currency, company=self.company)
|
|
||||||
|
|
||||||
sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate)
|
|
||||||
sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate)
|
|
||||||
|
|
||||||
if sle.name:
|
|
||||||
frappe.db.sql("""
|
|
||||||
update
|
|
||||||
`tabStock Ledger Entry`
|
|
||||||
set
|
|
||||||
stock_value = %(stock_value)s,
|
|
||||||
valuation_rate = %(valuation_rate)s,
|
|
||||||
stock_value_difference = %(stock_value_difference)s
|
|
||||||
where
|
|
||||||
name = %(name)s""", (sle))
|
|
||||||
|
|
||||||
return sle
|
|
||||||
|
|
||||||
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
||||||
if self.doctype == "Stock Reconciliation":
|
if self.doctype == "Stock Reconciliation":
|
||||||
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
|
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object):
|
|||||||
|
|
||||||
if not item.qty and self.doc.get("is_return"):
|
if not item.qty and self.doc.get("is_return"):
|
||||||
item.amount = flt(-1 * item.rate, item.precision("amount"))
|
item.amount = flt(-1 * item.rate, item.precision("amount"))
|
||||||
|
elif not item.qty and self.doc.get("is_debit_note"):
|
||||||
|
item.amount = flt(item.rate, item.precision("amount"))
|
||||||
else:
|
else:
|
||||||
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
||||||
|
|
||||||
@@ -594,13 +596,14 @@ class calculate_taxes_and_totals(object):
|
|||||||
|
|
||||||
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
grand_total = self.doc.rounded_total or self.doc.grand_total
|
||||||
|
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
|
||||||
|
|
||||||
if self.doc.party_account_currency == self.doc.currency:
|
if self.doc.party_account_currency == self.doc.currency:
|
||||||
total_amount_to_pay = flt(grand_total - self.doc.total_advance
|
total_amount_to_pay = flt(grand_total - self.doc.total_advance
|
||||||
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
|
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
|
||||||
else:
|
else:
|
||||||
total_amount_to_pay = flt(flt(grand_total *
|
total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
|
||||||
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
|
- flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
|
||||||
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
|
|
||||||
|
|
||||||
self.doc.round_floats_in(self.doc, ["paid_amount"])
|
self.doc.round_floats_in(self.doc, ["paid_amount"])
|
||||||
change_amount = 0
|
change_amount = 0
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
from erpnext.controllers import queries
|
from erpnext.controllers import queries
|
||||||
|
|
||||||
|
|
||||||
@@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase):
|
|||||||
|
|
||||||
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
||||||
self.assertGreaterEqual(len(wh), 1)
|
self.assertGreaterEqual(len(wh), 1)
|
||||||
|
|
||||||
|
def test_default_uoms(self):
|
||||||
|
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
|
||||||
|
|||||||
@@ -4,19 +4,72 @@ import frappe
|
|||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
def test_reset_default_field_value(self):
|
def test_reset_default_field_value(self):
|
||||||
doc = frappe.get_doc({
|
doc = frappe.get_doc({
|
||||||
"doctype": "Purchase Receipt",
|
"doctype": "Purchase Receipt",
|
||||||
"set_warehouse": "Warehouse 1",
|
"set_warehouse": "Warehouse 1",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Same values
|
# Same values
|
||||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||||
|
|
||||||
# Mixed values
|
# Mixed values
|
||||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.assertEqual(doc.set_warehouse, None)
|
self.assertEqual(doc.set_warehouse, None)
|
||||||
|
|
||||||
|
def test_reset_default_field_value_in_mfg_stock_entry(self):
|
||||||
|
# manufacture stock entry with rows having blank source/target wh
|
||||||
|
se = frappe.get_doc(
|
||||||
|
doctype="Stock Entry",
|
||||||
|
purpose="Manufacture",
|
||||||
|
stock_entry_type="Manufacture",
|
||||||
|
company="_Test Company",
|
||||||
|
from_warehouse="_Test Warehouse - _TC",
|
||||||
|
to_warehouse="_Test Warehouse 1 - _TC",
|
||||||
|
items=[
|
||||||
|
frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
|
||||||
|
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
se.save()
|
||||||
|
|
||||||
|
# default fields must be untouched
|
||||||
|
self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
|
||||||
|
self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
|
||||||
|
|
||||||
|
se.delete()
|
||||||
|
|
||||||
|
def test_reset_default_field_value_in_transfer_stock_entry(self):
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
"doctype": "Stock Entry",
|
||||||
|
"purpose": "Material Receipt",
|
||||||
|
"from_warehouse": "Warehouse 1",
|
||||||
|
"to_warehouse": "Warehouse 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Same values
|
||||||
|
doc.items = [
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
|
||||||
|
]
|
||||||
|
|
||||||
|
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||||
|
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||||
|
self.assertEqual(doc.from_warehouse, "Warehouse 1")
|
||||||
|
self.assertEqual(doc.to_warehouse, "Warehouse 2")
|
||||||
|
|
||||||
|
# Mixed values in source wh
|
||||||
|
doc.items = [
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
|
||||||
|
]
|
||||||
|
|
||||||
|
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||||
|
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||||
|
self.assertEqual(doc.from_warehouse, None)
|
||||||
|
self.assertEqual(doc.to_warehouse, "Warehouse 2")
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2021, 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
|
||||||
|
|
||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
context.body_class = "product-page"
|
context.body_class = "product-page"
|
||||||
|
|
||||||
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
||||||
self.attributes = frappe.get_all("Item Variant Attribute",
|
self.attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
fields=["attribute", "attribute_value"],
|
fields=["attribute", "attribute_value"],
|
||||||
filters={"parent": self.item_code})
|
filters={"parent": self.item_code}
|
||||||
|
)
|
||||||
|
|
||||||
if self.slideshow:
|
if self.slideshow:
|
||||||
context.update(get_slideshow(self))
|
context.update(get_slideshow(self))
|
||||||
|
|
||||||
self.set_variant_context(context)
|
|
||||||
self.set_attribute_context(context)
|
|
||||||
self.set_disabled_attributes(context)
|
|
||||||
self.set_metatags(context)
|
self.set_metatags(context)
|
||||||
self.set_shopping_cart_data(context)
|
self.set_shopping_cart_data(context)
|
||||||
|
|
||||||
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def set_variant_context(self, context):
|
|
||||||
if not self.has_variants:
|
|
||||||
return
|
|
||||||
|
|
||||||
context.no_cache = True
|
|
||||||
variant = frappe.form_dict.variant
|
|
||||||
|
|
||||||
# load variants
|
|
||||||
# also used in set_attribute_context
|
|
||||||
context.variants = frappe.get_all(
|
|
||||||
"Item",
|
|
||||||
filters={
|
|
||||||
"variant_of": self.item_code,
|
|
||||||
"published_in_website": 1
|
|
||||||
},
|
|
||||||
order_by="name asc")
|
|
||||||
|
|
||||||
# the case when the item is opened for the first time from its list
|
|
||||||
if not variant and context.variants:
|
|
||||||
variant = context.variants[0]
|
|
||||||
|
|
||||||
if variant:
|
|
||||||
context.variant = frappe.get_doc("Item", variant)
|
|
||||||
fields = ("website_image", "website_image_alt", "web_long_description", "description",
|
|
||||||
"website_specifications")
|
|
||||||
|
|
||||||
for fieldname in fields:
|
|
||||||
if context.variant.get(fieldname):
|
|
||||||
value = context.variant.get(fieldname)
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = [d.as_dict() for d in value]
|
|
||||||
|
|
||||||
context[fieldname] = value
|
|
||||||
|
|
||||||
if self.slideshow and context.variant and context.variant.slideshow:
|
|
||||||
context.update(get_slideshow(context.variant))
|
|
||||||
|
|
||||||
|
|
||||||
def set_attribute_context(self, context):
|
|
||||||
if not self.has_variants:
|
|
||||||
return
|
|
||||||
|
|
||||||
attribute_values_available = {}
|
|
||||||
context.attribute_values = {}
|
|
||||||
context.selected_attributes = {}
|
|
||||||
|
|
||||||
# load attributes
|
|
||||||
self.set_selected_attributes(context.variants, context, attribute_values_available)
|
|
||||||
|
|
||||||
# filter attributes, order based on attribute table
|
|
||||||
item = frappe.get_cached_doc("Item", self.item_code)
|
|
||||||
self.set_attribute_values(item.attributes, context, attribute_values_available)
|
|
||||||
|
|
||||||
context.variant_info = json.dumps(context.variants)
|
|
||||||
|
|
||||||
def set_selected_attributes(self, variants, context, attribute_values_available):
|
def set_selected_attributes(self, variants, context, attribute_values_available):
|
||||||
for variant in variants:
|
for variant in variants:
|
||||||
variant.attributes = frappe.get_all(
|
variant.attributes = frappe.get_all(
|
||||||
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||||
values.append(attr_value.attribute_value)
|
values.append(attr_value.attribute_value)
|
||||||
|
|
||||||
def set_disabled_attributes(self, context):
|
|
||||||
"""Disable selection options of attribute combinations that do not result in a variant"""
|
|
||||||
|
|
||||||
if not self.attributes or not self.has_variants:
|
|
||||||
return
|
|
||||||
|
|
||||||
context.disabled_attributes = {}
|
|
||||||
attributes = [attr.attribute for attr in self.attributes]
|
|
||||||
|
|
||||||
def find_variant(combination):
|
|
||||||
for variant in context.variants:
|
|
||||||
if len(variant.attributes) < len(attributes):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "combination" not in variant:
|
|
||||||
ref_combination = []
|
|
||||||
|
|
||||||
for attr in variant.attributes:
|
|
||||||
idx = attributes.index(attr.attribute)
|
|
||||||
ref_combination.insert(idx, attr.attribute_value)
|
|
||||||
|
|
||||||
variant["combination"] = ref_combination
|
|
||||||
|
|
||||||
if not (set(combination) - set(variant["combination"])):
|
|
||||||
# check if the combination is a subset of a variant combination
|
|
||||||
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
|
|
||||||
return True
|
|
||||||
|
|
||||||
for i, attr in enumerate(self.attributes):
|
|
||||||
if i == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
combination_source = []
|
|
||||||
|
|
||||||
# loop through previous attributes
|
|
||||||
for prev_attr in self.attributes[:i]:
|
|
||||||
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
|
|
||||||
|
|
||||||
combination_source.append(context.attribute_values[attr.attribute])
|
|
||||||
|
|
||||||
for combination in itertools.product(*combination_source):
|
|
||||||
if not find_variant(combination):
|
|
||||||
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
|
|
||||||
|
|
||||||
def set_metatags(self, context):
|
def set_metatags(self, context):
|
||||||
context.metatags = frappe._dict({})
|
context.metatags = frappe._dict({})
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,10 @@ class ProductQuery:
|
|||||||
website_item_groups = frappe.db.get_all(
|
website_item_groups = frappe.db.get_all(
|
||||||
"Website Item",
|
"Website Item",
|
||||||
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
|
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
|
||||||
filters=[["Website Item Group", "item_group", "=", item_group]]
|
filters=[
|
||||||
|
["Website Item Group", "item_group", "=", item_group],
|
||||||
|
["published", "=", 1]
|
||||||
|
]
|
||||||
)
|
)
|
||||||
return website_item_groups
|
return website_item_groups
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
|
|||||||
val = frappe.cache().get_value('ordered_attribute_values_map')
|
val = frappe.cache().get_value('ordered_attribute_values_map')
|
||||||
if val: return val
|
if val: return val
|
||||||
|
|
||||||
all_attribute_values = frappe.db.get_all('Item Attribute Value',
|
all_attribute_values = frappe.get_all('Item Attribute Value',
|
||||||
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
||||||
|
|
||||||
ordered_attribute_values_map = frappe._dict({})
|
ordered_attribute_values_map = frappe._dict({})
|
||||||
@@ -57,25 +57,34 @@ class ItemVariantsCacheManager:
|
|||||||
def build_cache(self):
|
def build_cache(self):
|
||||||
parent_item_code = self.item_code
|
parent_item_code = self.item_code
|
||||||
|
|
||||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
attributes = [
|
||||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
a.attribute for a in frappe.get_all(
|
||||||
|
'Item Variant Attribute',
|
||||||
|
{'parent': parent_item_code},
|
||||||
|
['attribute'],
|
||||||
|
order_by='idx asc'
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
item_variants_data = frappe.db.get_all('Item Variant Attribute',
|
# join with Website Item
|
||||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
item_variants_data = frappe.get_all(
|
||||||
|
'Item Variant Attribute',
|
||||||
|
{'variant_of': parent_item_code},
|
||||||
|
['parent', 'attribute', 'attribute_value'],
|
||||||
order_by='name',
|
order_by='name',
|
||||||
as_list=1
|
as_list=1
|
||||||
)
|
)
|
||||||
|
|
||||||
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
|
disabled_items = set(
|
||||||
|
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
|
||||||
|
)
|
||||||
|
|
||||||
attribute_value_item_map = frappe._dict({})
|
attribute_value_item_map = frappe._dict()
|
||||||
item_attribute_value_map = frappe._dict({})
|
item_attribute_value_map = frappe._dict()
|
||||||
|
|
||||||
# dont consider variants that are unpublished
|
# dont consider variants that are disabled
|
||||||
# (either have no Website Item or are unpublished in Website Item)
|
# pull all other variants
|
||||||
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
|
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||||
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
|
|
||||||
|
|
||||||
for row in item_variants_data:
|
for row in item_variants_data:
|
||||||
item_code, attribute, attribute_value = row
|
item_code, attribute, attribute_value = row
|
||||||
|
|||||||
@@ -1,11 +1,96 @@
|
|||||||
# import frappe
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
import frappe
|
||||||
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
test_dependencies = ["Item"]
|
||||||
|
|
||||||
class TestVariantSelector(unittest.TestCase):
|
class TestVariantSelector(unittest.TestCase):
|
||||||
# TODO: Variant Selector Tests
|
|
||||||
pass
|
def setUp(self) -> None:
|
||||||
|
self.template_item = make_item("Test-Tshirt-Temp", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"attribute": "Test Size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attribute": "Test Colour"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# create L-R, L-G, M-R, M-G and S-R
|
||||||
|
for size in ("Large", "Medium",):
|
||||||
|
for colour in ("Red", "Green",):
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": size,
|
||||||
|
"Test Colour": colour
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": "Small",
|
||||||
|
"Test Colour": "Red"
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_item_attributes(self):
|
||||||
|
"""
|
||||||
|
Test if the right attributes are fetched in the popup.
|
||||||
|
(Attributes must only come from active items)
|
||||||
|
|
||||||
|
Attribute selection must not be linked to Website Items.
|
||||||
|
"""
|
||||||
|
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
|
||||||
|
|
||||||
|
make_website_item(self.template_item) # publish template not variants
|
||||||
|
|
||||||
|
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||||
|
|
||||||
|
self.assertEqual(attr_data[0]["attribute"], "Test Size")
|
||||||
|
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
|
||||||
|
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
|
||||||
|
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
|
||||||
|
|
||||||
|
# disable small red tshirt, now there are no small tshirts.
|
||||||
|
# but there are some red tshirts
|
||||||
|
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
|
||||||
|
small_variant.disabled = 1
|
||||||
|
small_variant.save() # trigger cache rebuild
|
||||||
|
|
||||||
|
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||||
|
|
||||||
|
# Only L and M attribute values must be fetched since S is disabled
|
||||||
|
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
small_variant.disabled = 1
|
||||||
|
small_variant.save()
|
||||||
|
|
||||||
|
def test_next_item_variant_values(self):
|
||||||
|
"""
|
||||||
|
Test if on selecting an attribute value, the next possible values
|
||||||
|
are filtered accordingly.
|
||||||
|
Values that dont apply should not be fetched.
|
||||||
|
E.g.
|
||||||
|
There is a ** Small-Red ** Tshirt. No other colour in this size.
|
||||||
|
On selecting ** Small **, only ** Red ** should be selectable next.
|
||||||
|
"""
|
||||||
|
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||||
|
|
||||||
|
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
|
||||||
|
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
|
||||||
|
filtered_items = next_values["filtered_items"]
|
||||||
|
|
||||||
|
self.assertEqual(len(next_colours), 1)
|
||||||
|
self.assertEqual(next_colours.pop(), "Red")
|
||||||
|
self.assertEqual(len(filtered_items), 1)
|
||||||
|
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class Patient(Document):
|
|||||||
age = self.age
|
age = self.age
|
||||||
if not age:
|
if not age:
|
||||||
return
|
return
|
||||||
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
|
age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
|
||||||
return age_str
|
return age_str
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate
|
from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.hr.utils import validate_active_employee
|
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
|
||||||
|
|
||||||
|
|
||||||
class Attendance(Document):
|
class Attendance(Document):
|
||||||
@@ -171,7 +171,7 @@ def get_month_map():
|
|||||||
})
|
})
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_unmarked_days(employee, month):
|
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||||
import calendar
|
import calendar
|
||||||
month_map = get_month_map()
|
month_map = get_month_map()
|
||||||
|
|
||||||
@@ -191,6 +191,11 @@ def get_unmarked_days(employee, month):
|
|||||||
])
|
])
|
||||||
|
|
||||||
marked_days = [get_datetime(record.attendance_date) for record in records]
|
marked_days = [get_datetime(record.attendance_date) for record in records]
|
||||||
|
if cint(exclude_holidays):
|
||||||
|
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
|
||||||
|
holidays = [get_datetime(record) for record in holiday_dates]
|
||||||
|
marked_days.extend(holidays)
|
||||||
|
|
||||||
unmarked_days = []
|
unmarked_days = []
|
||||||
|
|
||||||
for date in dates_of_month:
|
for date in dates_of_month:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
onchange: function() {
|
onchange: function() {
|
||||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||||
dialog.set_df_property("status", "hidden", 1);
|
dialog.set_df_property("status", "hidden", 1);
|
||||||
|
dialog.set_df_property("exclude_holidays", "hidden", 1);
|
||||||
dialog.set_df_property("month", "value", '');
|
dialog.set_df_property("month", "value", '');
|
||||||
dialog.set_df_property("unmarked_days", "options", []);
|
dialog.set_df_property("unmarked_days", "options", []);
|
||||||
dialog.no_unmarked_days_left = false;
|
dialog.no_unmarked_days_left = false;
|
||||||
@@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
onchange: function() {
|
onchange: function() {
|
||||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||||
dialog.set_df_property("status", "hidden", 0);
|
dialog.set_df_property("status", "hidden", 0);
|
||||||
|
dialog.set_df_property("exclude_holidays", "hidden", 0);
|
||||||
dialog.set_df_property("unmarked_days", "options", []);
|
dialog.set_df_property("unmarked_days", "options", []);
|
||||||
dialog.no_unmarked_days_left = false;
|
dialog.no_unmarked_days_left = false;
|
||||||
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
|
me.get_multi_select_options(
|
||||||
|
dialog.fields_dict.employee.value,
|
||||||
|
dialog.fields_dict.month.value,
|
||||||
|
dialog.fields_dict.exclude_holidays.get_value()
|
||||||
|
).then(options => {
|
||||||
if (options.length > 0) {
|
if (options.length > 0) {
|
||||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||||
dialog.set_df_property("unmarked_days", "options", options);
|
dialog.set_df_property("unmarked_days", "options", options);
|
||||||
@@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
reqd: 1,
|
reqd: 1,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: __("Exclude Holidays"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
fieldname: "exclude_holidays",
|
||||||
|
hidden: 1,
|
||||||
|
onchange: function() {
|
||||||
|
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||||
|
dialog.set_df_property("status", "hidden", 0);
|
||||||
|
dialog.set_df_property("unmarked_days", "options", []);
|
||||||
|
dialog.no_unmarked_days_left = false;
|
||||||
|
me.get_multi_select_options(
|
||||||
|
dialog.fields_dict.employee.value,
|
||||||
|
dialog.fields_dict.month.value,
|
||||||
|
dialog.fields_dict.exclude_holidays.get_value()
|
||||||
|
).then(options => {
|
||||||
|
if (options.length > 0) {
|
||||||
|
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||||
|
dialog.set_df_property("unmarked_days", "options", options);
|
||||||
|
} else {
|
||||||
|
dialog.no_unmarked_days_left = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: __("Unmarked Attendance for days"),
|
label: __("Unmarked Attendance for days"),
|
||||||
fieldname: "unmarked_days",
|
fieldname: "unmarked_days",
|
||||||
@@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
get_multi_select_options: function(employee, month) {
|
get_multi_select_options: function(employee, month, exclude_holidays) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||||
@@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
args: {
|
args: {
|
||||||
employee: employee,
|
employee: employee,
|
||||||
month: month,
|
month: month,
|
||||||
|
exclude_holidays: exclude_holidays
|
||||||
}
|
}
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
var options = [];
|
var options = [];
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
|||||||
def test_employee_onboarding_incomplete_task(self):
|
def test_employee_onboarding_incomplete_task(self):
|
||||||
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
|
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
|
||||||
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
|
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
|
||||||
_set_up()
|
frappe.db.sql("delete from `tabEmployee Onboarding`")
|
||||||
|
project = "Employee Onboarding : test@researcher.com"
|
||||||
|
frappe.db.sql("delete from tabProject where name=%s", project)
|
||||||
|
frappe.db.sql("delete from tabTask where project=%s", project)
|
||||||
applicant = get_job_applicant()
|
applicant = get_job_applicant()
|
||||||
|
|
||||||
job_offer = create_job_offer(job_applicant=applicant.name)
|
job_offer = create_job_offer(job_applicant=applicant.name)
|
||||||
@@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
|||||||
onboarding.submit()
|
onboarding.submit()
|
||||||
|
|
||||||
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
|
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
|
||||||
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
|
self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
|
||||||
|
|
||||||
# don't allow making employee if onboarding is not complete
|
# don't allow making employee if onboarding is not complete
|
||||||
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
||||||
@@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
|||||||
self.assertEqual(employee.employee_name, 'Test Researcher')
|
self.assertEqual(employee.employee_name, 'Test Researcher')
|
||||||
|
|
||||||
def get_job_applicant():
|
def get_job_applicant():
|
||||||
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
|
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
|
||||||
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
|
return frappe.get_doc('Job Applicant', 'test@researcher.com')
|
||||||
applicant = frappe.new_doc('Job Applicant')
|
applicant = frappe.new_doc('Job Applicant')
|
||||||
applicant.applicant_name = 'Test Researcher'
|
applicant.applicant_name = 'Test Researcher'
|
||||||
applicant.email_id = 'test@researcher.com'
|
applicant.email_id = 'test@researcher.com'
|
||||||
|
|||||||
@@ -192,10 +192,11 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-29 23:06:10.904260",
|
"modified": "2022-01-12 16:28:53.196881",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Job Applicant",
|
"name": "Job Applicant",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -210,10 +211,11 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "applicant_name",
|
"search_fields": "applicant_name, email_id, job_title, phone_number",
|
||||||
"sender_field": "email_id",
|
"sender_field": "email_id",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
|
"states": [],
|
||||||
"subject_field": "notes",
|
"subject_field": "notes",
|
||||||
"title_field": "applicant_name"
|
"title_field": "applicant_name"
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.model.naming import append_number_if_name_exists
|
||||||
from frappe.utils import validate_email_address
|
from frappe.utils import validate_email_address
|
||||||
|
|
||||||
from erpnext.hr.doctype.interview.interview import get_interviewers
|
from erpnext.hr.doctype.interview.interview import get_interviewers
|
||||||
@@ -21,10 +22,11 @@ class JobApplicant(Document):
|
|||||||
self.get("__onload").job_offer = job_offer[0].name
|
self.get("__onload").job_offer = job_offer[0].name
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
|
self.name = self.email_id
|
||||||
if not keys:
|
|
||||||
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
|
# applicant can apply more than once for a different job title or reapply
|
||||||
self.name = " - ".join(keys)
|
if frappe.db.exists("Job Applicant", self.name):
|
||||||
|
self.name = append_number_if_name_exists("Job Applicant", self.name)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.email_id:
|
if self.email_id:
|
||||||
|
|||||||
@@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
|
|||||||
|
|
||||||
|
|
||||||
class TestJobApplicant(unittest.TestCase):
|
class TestJobApplicant(unittest.TestCase):
|
||||||
pass
|
def test_job_applicant_naming(self):
|
||||||
|
applicant = frappe.get_doc({
|
||||||
|
"doctype": "Job Applicant",
|
||||||
|
"status": "Open",
|
||||||
|
"applicant_name": "_Test Applicant",
|
||||||
|
"email_id": "job_applicant_naming@example.com"
|
||||||
|
}).insert()
|
||||||
|
self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
|
||||||
|
|
||||||
|
applicant = frappe.get_doc({
|
||||||
|
"doctype": "Job Applicant",
|
||||||
|
"status": "Open",
|
||||||
|
"applicant_name": "_Test Applicant",
|
||||||
|
"email_id": "job_applicant_naming@example.com"
|
||||||
|
}).insert()
|
||||||
|
self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
def create_job_applicant(**args):
|
def create_job_applicant(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -1,294 +1,108 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "HR-LPR-.YYYY.-.#####",
|
"autoname": "HR-LPR-.YYYY.-.#####",
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-04-13 15:20:52.864288",
|
"creation": "2018-04-13 15:20:52.864288",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"is_active",
|
||||||
|
"column_break_3",
|
||||||
|
"company",
|
||||||
|
"optional_holiday_list"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "from_date",
|
"fieldname": "from_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "From Date",
|
"label": "From Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "to_date",
|
"fieldname": "to_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "To Date",
|
"label": "To Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "is_active",
|
"fieldname": "is_active",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Is Active"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Is Active",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "optional_holiday_list",
|
"fieldname": "optional_holiday_list",
|
||||||
"fieldtype": "Link",
|
"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": "Holiday List for Optional Leave",
|
"label": "Holiday List for Optional Leave",
|
||||||
"length": 0,
|
"options": "Holiday List"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Holiday List",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"links": [],
|
||||||
"hide_heading": 0,
|
"modified": "2022-01-13 13:28:12.951025",
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2019-05-30 16:15:43.305502",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Period",
|
"name": "Leave Period",
|
||||||
"name_case": "",
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR Manager",
|
"role": "HR Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR User",
|
"role": "HR User",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
"search_fields": "from_date, to_date, company",
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"states": [],
|
||||||
"track_seen": 0,
|
"track_changes": 1
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
||||||
@@ -113,10 +113,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-01 17:54:01.014509",
|
"modified": "2022-01-13 13:37:11.218882",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Policy Assignment",
|
"name": "Leave Policy Assignment",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -164,5 +165,7 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "employee_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -129,6 +129,8 @@ class LeavePolicyAssignment(Document):
|
|||||||
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
|
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
|
||||||
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
|
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
|
||||||
new_leaves_allocated = monthly_earned_leave * months_passed
|
new_leaves_allocated = monthly_earned_leave * months_passed
|
||||||
|
else:
|
||||||
|
new_leaves_allocated = 0
|
||||||
|
|
||||||
return new_leaves_allocated
|
return new_leaves_allocated
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
|||||||
if (cur_dialog.fields_dict.leave_period.value) {
|
if (cur_dialog.fields_dict.leave_period.value) {
|
||||||
me.set_effective_date();
|
me.set_effective_date();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
get_query() {
|
||||||
|
let filters = {"is_active": 1};
|
||||||
|
if (cur_dialog.fields_dict.company.value)
|
||||||
|
filters["company"] = cur_dialog.fields_dict.company.value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters: filters
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Column Break"
|
fieldtype: "Column Break"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import add_months, get_first_day, getdate
|
||||||
|
|
||||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||||
get_employee,
|
get_employee,
|
||||||
@@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
|||||||
test_dependencies = ["Employee"]
|
test_dependencies = ["Employee"]
|
||||||
|
|
||||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
class TestLeavePolicyAssignment(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
||||||
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
||||||
|
|
||||||
def test_grant_leaves(self):
|
def test_grant_leaves(self):
|
||||||
@@ -54,8 +54,8 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
|
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
|
||||||
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
|
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
|
||||||
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
|
self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date))
|
||||||
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
|
self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date))
|
||||||
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
|
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
|
||||||
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
|
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
|
||||||
|
|
||||||
@@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
|||||||
# User are now allowed to grant leave
|
# User are now allowed to grant leave
|
||||||
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
|
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
|
||||||
|
|
||||||
|
def test_earned_leave_allocation(self):
|
||||||
|
leave_period = create_leave_period("Test Earned Leave Period")
|
||||||
|
employee = get_employee()
|
||||||
|
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||||
|
|
||||||
|
leave_policy = frappe.get_doc({
|
||||||
|
"doctype": "Leave Policy",
|
||||||
|
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"assignment_based_on": "Leave Period",
|
||||||
|
"leave_policy": leave_policy.name,
|
||||||
|
"leave_period": leave_period.name
|
||||||
|
}
|
||||||
|
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||||
|
|
||||||
|
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
|
||||||
|
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||||
|
"leave_policy_assignment": leave_policy_assignments[0]
|
||||||
|
}, "total_leaves_allocated")
|
||||||
|
self.assertEqual(leaves_allocated, 0)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
frappe.db.rollback()
|
||||||
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
|
||||||
|
|
||||||
|
def create_earned_leave_type(leave_type):
|
||||||
|
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||||
|
|
||||||
|
return frappe.get_doc(dict(
|
||||||
|
leave_type_name=leave_type,
|
||||||
|
doctype="Leave Type",
|
||||||
|
is_earned_leave=1,
|
||||||
|
earned_leave_frequency="Monthly",
|
||||||
|
rounding=0.5,
|
||||||
|
max_leaves_allowed=6
|
||||||
|
)).insert()
|
||||||
|
|
||||||
|
|
||||||
|
def create_leave_period(name):
|
||||||
|
frappe.delete_doc_if_exists("Leave Period", name, force=1)
|
||||||
|
start_date = get_first_day(getdate())
|
||||||
|
|
||||||
|
return frappe.get_doc(dict(
|
||||||
|
name=name,
|
||||||
|
doctype="Leave Period",
|
||||||
|
from_date=start_date,
|
||||||
|
to_date=add_months(start_date, 12),
|
||||||
|
company="_Test Company",
|
||||||
|
is_active=1
|
||||||
|
)).insert()
|
||||||
@@ -4,15 +4,32 @@
|
|||||||
frappe.ui.form.on('Shift Type', {
|
frappe.ui.form.on('Shift Type', {
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
'Mark Attendance',
|
__('Mark Attendance'),
|
||||||
() => frm.call({
|
() => {
|
||||||
doc: frm.doc,
|
if (!frm.doc.enable_auto_attendance) {
|
||||||
method: 'process_auto_attendance',
|
frm.scroll_to_field('enable_auto_attendance');
|
||||||
freeze: true,
|
frappe.throw(__('Please Enable Auto Attendance and complete the setup first.'));
|
||||||
callback: () => {
|
|
||||||
frappe.msgprint(__("Attendance has been marked as per employee check-ins"));
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
if (!frm.doc.process_attendance_after) {
|
||||||
|
frm.scroll_to_field('process_attendance_after');
|
||||||
|
frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frm.doc.last_sync_of_checkin) {
|
||||||
|
frm.scroll_to_field('last_sync_of_checkin');
|
||||||
|
frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'process_auto_attendance',
|
||||||
|
freeze: true,
|
||||||
|
callback: () => {
|
||||||
|
frappe.msgprint(__('Attendance has been marked as per employee check-ins'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"company",
|
"company",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"is_term_loan",
|
|
||||||
"rate_of_interest",
|
"rate_of_interest",
|
||||||
|
"payroll_payable_account",
|
||||||
|
"is_term_loan",
|
||||||
|
"repay_from_salary",
|
||||||
"payment_details_section",
|
"payment_details_section",
|
||||||
"due_date",
|
"due_date",
|
||||||
"pending_principal_amount",
|
"pending_principal_amount",
|
||||||
@@ -243,15 +245,31 @@
|
|||||||
"label": "Total Penalty Paid",
|
"label": "Total Penalty Paid",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.repay_from_salary",
|
||||||
|
"fieldname": "payroll_payable_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Payroll Payable Account",
|
||||||
|
"mandatory_depends_on": "eval:doc.repay_from_salary",
|
||||||
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "against_loan.repay_from_salary",
|
||||||
|
"fieldname": "repay_from_salary",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Repay From Salary"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-19 18:10:00.935364",
|
"modified": "2022-01-06 01:51:06.707782",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan Repayment",
|
"name": "Loan Repayment",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -287,5 +305,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -321,74 +321,79 @@ class LoanRepayment(AccountsController):
|
|||||||
else:
|
else:
|
||||||
remarks = _("Repayment against Loan: ") + self.against_loan
|
remarks = _("Repayment against Loan: ") + self.against_loan
|
||||||
|
|
||||||
if not loan_details.repay_from_salary:
|
if self.repay_from_salary:
|
||||||
if self.total_penalty_paid:
|
payment_account = self.payroll_payable_account
|
||||||
gle_map.append(
|
else:
|
||||||
self.get_gl_dict({
|
payment_account = loan_details.payment_account
|
||||||
"account": loan_details.loan_account,
|
|
||||||
"against": loan_details.payment_account,
|
|
||||||
"debit": self.total_penalty_paid,
|
|
||||||
"debit_in_account_currency": self.total_penalty_paid,
|
|
||||||
"against_voucher_type": "Loan",
|
|
||||||
"against_voucher": self.against_loan,
|
|
||||||
"remarks": _("Penalty against loan:") + self.against_loan,
|
|
||||||
"cost_center": self.cost_center,
|
|
||||||
"party_type": self.applicant_type,
|
|
||||||
"party": self.applicant,
|
|
||||||
"posting_date": getdate(self.posting_date)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
gle_map.append(
|
|
||||||
self.get_gl_dict({
|
|
||||||
"account": loan_details.penalty_income_account,
|
|
||||||
"against": loan_details.payment_account,
|
|
||||||
"credit": self.total_penalty_paid,
|
|
||||||
"credit_in_account_currency": self.total_penalty_paid,
|
|
||||||
"against_voucher_type": "Loan",
|
|
||||||
"against_voucher": self.against_loan,
|
|
||||||
"remarks": _("Penalty against loan:") + self.against_loan,
|
|
||||||
"cost_center": self.cost_center,
|
|
||||||
"posting_date": getdate(self.posting_date)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
gle_map.append(
|
|
||||||
self.get_gl_dict({
|
|
||||||
"account": loan_details.payment_account,
|
|
||||||
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
|
|
||||||
+ ", " + loan_details.penalty_income_account,
|
|
||||||
"debit": self.amount_paid,
|
|
||||||
"debit_in_account_currency": self.amount_paid,
|
|
||||||
"against_voucher_type": "Loan",
|
|
||||||
"against_voucher": self.against_loan,
|
|
||||||
"remarks": remarks,
|
|
||||||
"cost_center": self.cost_center,
|
|
||||||
"posting_date": getdate(self.posting_date)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if self.total_penalty_paid:
|
||||||
gle_map.append(
|
gle_map.append(
|
||||||
self.get_gl_dict({
|
self.get_gl_dict({
|
||||||
"account": loan_details.loan_account,
|
"account": loan_details.loan_account,
|
||||||
"party_type": loan_details.applicant_type,
|
|
||||||
"party": loan_details.applicant,
|
|
||||||
"against": loan_details.payment_account,
|
"against": loan_details.payment_account,
|
||||||
"credit": self.amount_paid,
|
"debit": self.total_penalty_paid,
|
||||||
"credit_in_account_currency": self.amount_paid,
|
"debit_in_account_currency": self.total_penalty_paid,
|
||||||
"against_voucher_type": "Loan",
|
"against_voucher_type": "Loan",
|
||||||
"against_voucher": self.against_loan,
|
"against_voucher": self.against_loan,
|
||||||
"remarks": remarks,
|
"remarks": _("Penalty against loan:") + self.against_loan,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
"party_type": self.applicant_type,
|
||||||
|
"party": self.applicant,
|
||||||
|
"posting_date": getdate(self.posting_date)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
gle_map.append(
|
||||||
|
self.get_gl_dict({
|
||||||
|
"account": loan_details.penalty_income_account,
|
||||||
|
"against": payment_account,
|
||||||
|
"credit": self.total_penalty_paid,
|
||||||
|
"credit_in_account_currency": self.total_penalty_paid,
|
||||||
|
"against_voucher_type": "Loan",
|
||||||
|
"against_voucher": self.against_loan,
|
||||||
|
"remarks": _("Penalty against loan:") + self.against_loan,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date)
|
"posting_date": getdate(self.posting_date)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if gle_map:
|
gle_map.append(
|
||||||
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
|
self.get_gl_dict({
|
||||||
|
"account": payment_account,
|
||||||
|
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
|
||||||
|
+ ", " + loan_details.penalty_income_account,
|
||||||
|
"debit": self.amount_paid,
|
||||||
|
"debit_in_account_currency": self.amount_paid,
|
||||||
|
"against_voucher_type": "Loan",
|
||||||
|
"against_voucher": self.against_loan,
|
||||||
|
"remarks": remarks,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
"posting_date": getdate(self.posting_date)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
gle_map.append(
|
||||||
|
self.get_gl_dict({
|
||||||
|
"account": loan_details.loan_account,
|
||||||
|
"party_type": loan_details.applicant_type,
|
||||||
|
"party": loan_details.applicant,
|
||||||
|
"against": payment_account,
|
||||||
|
"credit": self.amount_paid,
|
||||||
|
"credit_in_account_currency": self.amount_paid,
|
||||||
|
"against_voucher_type": "Loan",
|
||||||
|
"against_voucher": self.against_loan,
|
||||||
|
"remarks": remarks,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
"posting_date": getdate(self.posting_date)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if gle_map:
|
||||||
|
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
|
||||||
|
|
||||||
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
|
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
|
||||||
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):
|
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None,
|
||||||
|
payroll_payable_account=None):
|
||||||
|
|
||||||
lr = frappe.get_doc({
|
lr = frappe.get_doc({
|
||||||
"doctype": "Loan Repayment",
|
"doctype": "Loan Repayment",
|
||||||
@@ -401,7 +406,8 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
|
|||||||
"interest_payable": interest_payable,
|
"interest_payable": interest_payable,
|
||||||
"payable_principal_amount": payable_principal_amount,
|
"payable_principal_amount": payable_principal_amount,
|
||||||
"amount_paid": amount_paid,
|
"amount_paid": amount_paid,
|
||||||
"loan_type": loan_type
|
"loan_type": loan_type,
|
||||||
|
"payroll_payable_account": payroll_payable_account
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
return lr
|
return lr
|
||||||
|
|||||||
@@ -531,16 +531,6 @@ class BOM(WebsiteGenerator):
|
|||||||
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
||||||
if self.conversion_rate and hour_rate else hour_rate)
|
if self.conversion_rate and hour_rate else hour_rate)
|
||||||
|
|
||||||
if self.routing:
|
|
||||||
time_in_mins = flt(frappe.db.get_value("BOM Operation", {
|
|
||||||
"workstation": row.workstation,
|
|
||||||
"operation": row.operation,
|
|
||||||
"parent": self.routing
|
|
||||||
}, ["time_in_mins"]))
|
|
||||||
|
|
||||||
if time_in_mins:
|
|
||||||
row.time_in_mins = time_in_mins
|
|
||||||
|
|
||||||
if row.hour_rate and row.time_in_mins:
|
if row.hour_rate and row.time_in_mins:
|
||||||
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
||||||
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase):
|
|||||||
wo_doc.delete()
|
wo_doc.delete()
|
||||||
|
|
||||||
def test_update_bom_operation_time(self):
|
def test_update_bom_operation_time(self):
|
||||||
|
"""Update cost shouldn't update routing times."""
|
||||||
operations = [
|
operations = [
|
||||||
{
|
{
|
||||||
"operation": "Test Operation A",
|
"operation": "Test Operation A",
|
||||||
@@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase):
|
|||||||
routing_doc.save()
|
routing_doc.save()
|
||||||
bom_doc.update_cost()
|
bom_doc.update_cost()
|
||||||
bom_doc.reload()
|
bom_doc.reload()
|
||||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
|
self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
|
||||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
|
self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
|
||||||
|
|
||||||
|
|
||||||
def setup_operations(rows):
|
def setup_operations(rows):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, cint, flt, now, today
|
from frappe.utils import add_days, add_months, cint, flt, now, today
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
@@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
|||||||
OverProductionError,
|
OverProductionError,
|
||||||
StockOverProductionError,
|
StockOverProductionError,
|
||||||
close_work_order,
|
close_work_order,
|
||||||
|
make_job_card,
|
||||||
make_stock_entry,
|
make_stock_entry,
|
||||||
stop_unstop,
|
stop_unstop,
|
||||||
)
|
)
|
||||||
@@ -801,6 +802,34 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
if row.is_scrap_item:
|
if row.is_scrap_item:
|
||||||
self.assertEqual(row.qty, 1)
|
self.assertEqual(row.qty, 1)
|
||||||
|
|
||||||
|
# Partial Job Card 1 with qty 10
|
||||||
|
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1)
|
||||||
|
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
|
||||||
|
update_job_card(job_card, 10)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_scrap_item:
|
||||||
|
self.assertEqual(row.qty, 2)
|
||||||
|
|
||||||
|
# Partial Job Card 2 with qty 10
|
||||||
|
operations = []
|
||||||
|
wo_order.load_from_db()
|
||||||
|
for row in wo_order.operations:
|
||||||
|
n_dict = row.as_dict()
|
||||||
|
n_dict['qty'] = 10
|
||||||
|
n_dict['pending_qty'] = 10
|
||||||
|
operations.append(n_dict)
|
||||||
|
|
||||||
|
make_job_card(wo_order.name, operations)
|
||||||
|
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name')
|
||||||
|
update_job_card(job_card, 10)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_scrap_item:
|
||||||
|
self.assertEqual(row.qty, 2)
|
||||||
|
|
||||||
def test_close_work_order(self):
|
def test_close_work_order(self):
|
||||||
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
|
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
|
||||||
'Test RM Item 2 for Closed WO']
|
'Test RM Item 2 for Closed WO']
|
||||||
@@ -841,7 +870,9 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
close_work_order(wo_order, "Closed")
|
close_work_order(wo_order, "Closed")
|
||||||
self.assertEqual(wo_order.get('status'), "Closed")
|
self.assertEqual(wo_order.get('status'), "Closed")
|
||||||
|
|
||||||
def update_job_card(job_card):
|
def update_job_card(job_card, jc_qty=None):
|
||||||
|
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
|
||||||
|
|
||||||
job_card_doc = frappe.get_doc('Job Card', job_card)
|
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||||
job_card_doc.set('scrap_items', [
|
job_card_doc.set('scrap_items', [
|
||||||
{
|
{
|
||||||
@@ -854,15 +885,18 @@ def update_job_card(job_card):
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if jc_qty:
|
||||||
|
job_card_doc.for_quantity = jc_qty
|
||||||
|
|
||||||
job_card_doc.append('time_logs', {
|
job_card_doc.append('time_logs', {
|
||||||
'from_time': now(),
|
'from_time': now(),
|
||||||
|
'employee': employee,
|
||||||
'time_in_mins': 60,
|
'time_in_mins': 60,
|
||||||
'completed_qty': job_card_doc.for_quantity
|
'completed_qty': job_card_doc.for_quantity
|
||||||
})
|
})
|
||||||
|
|
||||||
job_card_doc.submit()
|
job_card_doc.submit()
|
||||||
|
|
||||||
|
|
||||||
def get_scrap_item_details(bom_no):
|
def get_scrap_item_details(bom_no):
|
||||||
scrap_items = {}
|
scrap_items = {}
|
||||||
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
||||||
|
|||||||
@@ -131,16 +131,14 @@ frappe.ui.form.on("Work Order", {
|
|||||||
erpnext.work_order.set_custom_buttons(frm);
|
erpnext.work_order.set_custom_buttons(frm);
|
||||||
frm.set_intro("");
|
frm.set_intro("");
|
||||||
|
|
||||||
if (frm.doc.docstatus === 0 && !frm.doc.__islocal) {
|
if (frm.doc.docstatus === 0 && !frm.is_new()) {
|
||||||
frm.set_intro(__("Submit this Work Order for further processing."));
|
frm.set_intro(__("Submit this Work Order for further processing."));
|
||||||
|
} else {
|
||||||
|
frm.trigger("show_progress_for_items");
|
||||||
|
frm.trigger("show_progress_for_operations");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.status != "Closed") {
|
if (frm.doc.status != "Closed") {
|
||||||
if (frm.doc.docstatus===1) {
|
|
||||||
frm.trigger('show_progress_for_items');
|
|
||||||
frm.trigger('show_progress_for_operations');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frm.doc.docstatus === 1
|
if (frm.doc.docstatus === 1
|
||||||
&& frm.doc.operations && frm.doc.operations.length) {
|
&& frm.doc.operations && frm.doc.operations.length) {
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list
|
|||||||
erpnext.patches.v12_0.set_default_payroll_based_on
|
erpnext.patches.v12_0.set_default_payroll_based_on
|
||||||
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
|
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
|
||||||
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
|
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
|
||||||
erpnext.patches.v13_0.validate_options_for_data_field
|
|
||||||
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
|
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
|
||||||
erpnext.patches.v12_0.fix_quotation_expired_status
|
erpnext.patches.v12_0.fix_quotation_expired_status
|
||||||
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
|
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
|
||||||
@@ -306,6 +305,7 @@ erpnext.patches.v13_0.shopify_deprecation_warning
|
|||||||
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
|
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
|
||||||
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
|
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
|
||||||
erpnext.patches.v13_0.remove_bad_selling_defaults
|
erpnext.patches.v13_0.remove_bad_selling_defaults
|
||||||
|
erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022
|
||||||
erpnext.patches.v13_0.migrate_stripe_api
|
erpnext.patches.v13_0.migrate_stripe_api
|
||||||
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
|
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
|
||||||
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
|
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
|
||||||
@@ -331,6 +331,7 @@ erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
|
|||||||
erpnext.patches.v13_0.requeue_failed_reposts
|
erpnext.patches.v13_0.requeue_failed_reposts
|
||||||
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
||||||
erpnext.patches.v13_0.update_job_card_status
|
erpnext.patches.v13_0.update_job_card_status
|
||||||
|
erpnext.patches.v13_0.enable_uoms
|
||||||
erpnext.patches.v12_0.update_production_plan_status
|
erpnext.patches.v12_0.update_production_plan_status
|
||||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||||
@@ -339,3 +340,4 @@ erpnext.patches.v13_0.rename_ksa_qr_field
|
|||||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
||||||
erpnext.patches.v13_0.update_tax_category_for_rcm
|
erpnext.patches.v13_0.update_tax_category_for_rcm
|
||||||
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||||
|
erpnext.patches.v13_0.agriculture_deprecation_warning
|
||||||
|
|||||||
10
erpnext/patches/v13_0/agriculture_deprecation_warning.py
Normal file
10
erpnext/patches/v13_0/agriculture_deprecation_warning.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
|
||||||
|
click.secho(
|
||||||
|
"Agriculture Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
|
||||||
|
"When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/agriculture",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
13
erpnext/patches/v13_0/enable_uoms.py
Normal file
13
erpnext/patches/v13_0/enable_uoms.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('setup', 'doctype', 'uom')
|
||||||
|
|
||||||
|
uom = frappe.qb.DocType("UOM")
|
||||||
|
|
||||||
|
(frappe.qb
|
||||||
|
.update(uom)
|
||||||
|
.set(uom.enabled, 1)
|
||||||
|
.where(uom.creation >= "2021-10-18") # date when this field was released
|
||||||
|
).run()
|
||||||
67
erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
Normal file
67
erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
broken_sles = frappe.db.sql("""
|
||||||
|
select name, serial_no
|
||||||
|
from `tabStock Ledger Entry`
|
||||||
|
where
|
||||||
|
is_cancelled = 0
|
||||||
|
and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s
|
||||||
|
or serial_no = %s )
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
" %", # leading whitespace
|
||||||
|
"% ", # trailing whitespace
|
||||||
|
"%\n %", # leading whitespace on newline
|
||||||
|
"% \n%", # trailing whitespace on newline
|
||||||
|
"\n", # just new line
|
||||||
|
),
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles)
|
||||||
|
|
||||||
|
if not broken_sles:
|
||||||
|
return
|
||||||
|
|
||||||
|
broken_serial_nos = set()
|
||||||
|
|
||||||
|
# patch SLEs
|
||||||
|
for sle in broken_sles:
|
||||||
|
serial_no_list = get_serial_nos(sle.serial_no)
|
||||||
|
correct_sr_no = "\n".join(serial_no_list)
|
||||||
|
|
||||||
|
if correct_sr_no == sle.serial_no:
|
||||||
|
continue
|
||||||
|
|
||||||
|
frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False)
|
||||||
|
broken_serial_nos.update(serial_no_list)
|
||||||
|
|
||||||
|
if not broken_serial_nos:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Patch serial No documents if they don't have purchase info
|
||||||
|
# Purchase info is used for fetching incoming rate
|
||||||
|
broken_sr_no_records = frappe.get_list("Serial No",
|
||||||
|
filters={
|
||||||
|
"status":"Active",
|
||||||
|
"name": ("in", broken_serial_nos),
|
||||||
|
"purchase_document_type": ("is", "not set")
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records)
|
||||||
|
|
||||||
|
patch_savepoint = "serial_no_patch"
|
||||||
|
for serial_no in broken_sr_no_records:
|
||||||
|
try:
|
||||||
|
frappe.db.savepoint(patch_savepoint)
|
||||||
|
sn = frappe.get_doc("Serial No", serial_no)
|
||||||
|
sn.update_serial_no_reference()
|
||||||
|
sn.db_update()
|
||||||
|
except Exception:
|
||||||
|
frappe.db.rollback(save_point=patch_savepoint)
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model import data_field_options
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
|
|
||||||
for field in frappe.get_all('Custom Field',
|
|
||||||
fields = ['name'],
|
|
||||||
filters = {
|
|
||||||
'fieldtype': 'Data',
|
|
||||||
'options': ['!=', None]
|
|
||||||
}):
|
|
||||||
|
|
||||||
if field not in data_field_options:
|
|
||||||
frappe.db.sql("""
|
|
||||||
UPDATE
|
|
||||||
`tabCustom Field`
|
|
||||||
SET
|
|
||||||
options=NULL
|
|
||||||
WHERE
|
|
||||||
name=%s
|
|
||||||
""", (field))
|
|
||||||
@@ -60,6 +60,8 @@ class PayrollEntry(Document):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||||
where payroll_entry=%s """, (self.name)))
|
where payroll_entry=%s """, (self.name)))
|
||||||
|
self.db_set("salary_slips_created", 0)
|
||||||
|
self.db_set("salary_slips_submitted", 0)
|
||||||
|
|
||||||
def get_emp_list(self):
|
def get_emp_list(self):
|
||||||
"""
|
"""
|
||||||
@@ -350,23 +352,24 @@ class PayrollEntry(Document):
|
|||||||
currencies = []
|
currencies = []
|
||||||
multi_currency = 0
|
multi_currency = 0
|
||||||
company_currency = erpnext.get_company_currency(self.company)
|
company_currency = erpnext.get_company_currency(self.company)
|
||||||
|
accounting_dimensions = get_accounting_dimensions() or []
|
||||||
|
|
||||||
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies)
|
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies)
|
||||||
accounts.append({
|
accounts.append(self.update_accounting_dimensions({
|
||||||
"account": self.payment_account,
|
"account": self.payment_account,
|
||||||
"bank_account": self.bank_account,
|
"bank_account": self.bank_account,
|
||||||
"credit_in_account_currency": flt(amount, precision),
|
"credit_in_account_currency": flt(amount, precision),
|
||||||
"exchange_rate": flt(exchange_rate),
|
"exchange_rate": flt(exchange_rate),
|
||||||
})
|
}, accounting_dimensions))
|
||||||
|
|
||||||
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies)
|
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies)
|
||||||
accounts.append({
|
accounts.append(self.update_accounting_dimensions({
|
||||||
"account": payroll_payable_account,
|
"account": payroll_payable_account,
|
||||||
"debit_in_account_currency": flt(amount, precision),
|
"debit_in_account_currency": flt(amount, precision),
|
||||||
"exchange_rate": flt(exchange_rate),
|
"exchange_rate": flt(exchange_rate),
|
||||||
"reference_type": self.doctype,
|
"reference_type": self.doctype,
|
||||||
"reference_name": self.name
|
"reference_name": self.name
|
||||||
})
|
}, accounting_dimensions))
|
||||||
|
|
||||||
if len(currencies) > 1:
|
if len(currencies) > 1:
|
||||||
multi_currency = 1
|
multi_currency = 1
|
||||||
|
|||||||
@@ -936,8 +936,11 @@ class SalarySlip(TransactionBase):
|
|||||||
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
|
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
|
||||||
future_recurring_additional_amount = 0
|
future_recurring_additional_amount = 0
|
||||||
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
|
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
|
||||||
|
|
||||||
# future month count excluding current
|
# future month count excluding current
|
||||||
future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
|
from_date, to_date = getdate(self.start_date), getdate(to_date)
|
||||||
|
future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
|
||||||
|
|
||||||
if future_recurring_period > 0:
|
if future_recurring_period > 0:
|
||||||
future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
|
future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
|
||||||
return future_recurring_additional_amount
|
return future_recurring_additional_amount
|
||||||
@@ -1036,7 +1039,8 @@ class SalarySlip(TransactionBase):
|
|||||||
data.update({"annual_taxable_earning": annual_taxable_earning})
|
data.update({"annual_taxable_earning": annual_taxable_earning})
|
||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
for slab in tax_slab.slabs:
|
for slab in tax_slab.slabs:
|
||||||
if slab.condition and not self.eval_tax_slab_condition(slab.condition, data):
|
cond = cstr(slab.condition).strip()
|
||||||
|
if cond and not self.eval_tax_slab_condition(cond, data):
|
||||||
continue
|
continue
|
||||||
if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
|
if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
|
||||||
tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
|
tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
|
||||||
@@ -1142,15 +1146,17 @@ class SalarySlip(TransactionBase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def make_loan_repayment_entry(self):
|
def make_loan_repayment_entry(self):
|
||||||
|
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
||||||
for loan in self.loans:
|
for loan in self.loans:
|
||||||
repayment_entry = create_repayment_entry(loan.loan, self.employee,
|
if loan.total_payment:
|
||||||
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
|
repayment_entry = create_repayment_entry(loan.loan, self.employee,
|
||||||
loan.principal_amount, loan.total_payment)
|
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
|
||||||
|
loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account)
|
||||||
|
|
||||||
repayment_entry.save()
|
repayment_entry.save()
|
||||||
repayment_entry.submit()
|
repayment_entry.submit()
|
||||||
|
|
||||||
frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
|
frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
|
||||||
|
|
||||||
def cancel_loan_repayment_entry(self):
|
def cancel_loan_repayment_entry(self):
|
||||||
for loan in self.loans:
|
for loan in self.loans:
|
||||||
@@ -1384,3 +1390,11 @@ def get_salary_component_data(component):
|
|||||||
],
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_payroll_payable_account(company, payroll_entry):
|
||||||
|
if payroll_entry:
|
||||||
|
payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account')
|
||||||
|
else:
|
||||||
|
payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account')
|
||||||
|
|
||||||
|
return payroll_payable_account
|
||||||
@@ -384,7 +384,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
|
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
|
||||||
payroll_period=payroll_period)
|
payroll_period=payroll_period)
|
||||||
|
|
||||||
frappe.db.sql("delete from tabLoan")
|
frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'")
|
||||||
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
|
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
|
||||||
loan.repay_from_salary = 1
|
loan.repay_from_salary = 1
|
||||||
loan.submit()
|
loan.submit()
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class Task(NestedSet):
|
|||||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||||
|
|
||||||
def update_depends_on(self):
|
def update_depends_on(self):
|
||||||
depends_on_tasks = self.depends_on_tasks or ""
|
depends_on_tasks = ""
|
||||||
for d in self.depends_on:
|
for d in self.depends_on:
|
||||||
if d.task and d.task not in depends_on_tasks:
|
if d.task and d.task not in depends_on_tasks:
|
||||||
depends_on_tasks += d.task + ","
|
depends_on_tasks += d.task + ","
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
|
|
||||||
if ((!item.qty) && me.frm.doc.is_return) {
|
if ((!item.qty) && me.frm.doc.is_return) {
|
||||||
item.amount = flt(item.rate * -1, precision("amount", item));
|
item.amount = flt(item.rate * -1, precision("amount", item));
|
||||||
|
} else if ((!item.qty) && me.frm.doc.is_debit_note) {
|
||||||
|
item.amount = flt(item.rate, precision("amount", item));
|
||||||
} else {
|
} else {
|
||||||
item.amount = flt(item.rate * item.qty, precision("amount", item));
|
item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||||
}
|
}
|
||||||
@@ -708,14 +710,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
|
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
|
||||||
|
|
||||||
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
|
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
|
||||||
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||||
|
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
|
||||||
|
|
||||||
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
||||||
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
||||||
- this.frm.doc.write_off_amount), precision("grand_total"));
|
- this.frm.doc.write_off_amount), precision("grand_total"));
|
||||||
} else {
|
} else {
|
||||||
var total_amount_to_pay = flt(
|
var total_amount_to_pay = flt(
|
||||||
(flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
|
(flt(base_grand_total, precision("base_grand_total"))
|
||||||
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
|
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
|
||||||
precision("base_grand_total")
|
precision("base_grand_total")
|
||||||
);
|
);
|
||||||
@@ -746,14 +749,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_total_amount_to_default_mop: function() {
|
set_total_amount_to_default_mop: function() {
|
||||||
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||||
|
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
|
||||||
|
|
||||||
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
||||||
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
||||||
- this.frm.doc.write_off_amount), precision("grand_total"));
|
- this.frm.doc.write_off_amount), precision("grand_total"));
|
||||||
} else {
|
} else {
|
||||||
var total_amount_to_pay = flt(
|
var total_amount_to_pay = flt(
|
||||||
(flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
|
(flt(base_grand_total, precision("base_grand_total"))
|
||||||
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
|
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
|
||||||
precision("base_grand_total")
|
precision("base_grand_total")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('UAE VAT Settings', {
|
frappe.ui.form.on('UAE VAT Settings', {
|
||||||
// refresh: function(frm) {
|
onload: function(frm) {
|
||||||
|
frm.set_query('account', 'uae_vat_accounts', function() {
|
||||||
// }
|
return {
|
||||||
|
filters: {
|
||||||
|
'company': frm.doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ def get_regional_address_details(party_details, doctype, company):
|
|||||||
|
|
||||||
if tax_template_by_category:
|
if tax_template_by_category:
|
||||||
party_details['taxes_and_charges'] = tax_template_by_category
|
party_details['taxes_and_charges'] = tax_template_by_category
|
||||||
return
|
return party_details
|
||||||
|
|
||||||
if not party_details.place_of_supply: return party_details
|
if not party_details.place_of_supply: return party_details
|
||||||
if not party_details.company_gstin: return party_details
|
if not party_details.company_gstin: return party_details
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = {
|
|||||||
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
|
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
|
||||||
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
|
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
|
||||||
{ "value": "EXPORT", "label": __("Export Invoice - 6A") },
|
{ "value": "EXPORT", "label": __("Export Invoice - 6A") },
|
||||||
{ "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") }
|
{ "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") },
|
||||||
|
{ "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") }
|
||||||
],
|
],
|
||||||
"default": "B2B"
|
"default": "B2B"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class Gstr1Report(object):
|
|||||||
port_code,
|
port_code,
|
||||||
shipping_bill_number,
|
shipping_bill_number,
|
||||||
shipping_bill_date,
|
shipping_bill_date,
|
||||||
reason_for_issuing_document
|
reason_for_issuing_document,
|
||||||
|
company_gstin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -63,6 +64,8 @@ class Gstr1Report(object):
|
|||||||
self.get_b2c_data()
|
self.get_b2c_data()
|
||||||
elif self.filters.get("type_of_business") == "Advances":
|
elif self.filters.get("type_of_business") == "Advances":
|
||||||
self.get_advance_data()
|
self.get_advance_data()
|
||||||
|
elif self.filters.get("type_of_business") == "NIL Rated":
|
||||||
|
self.get_nil_rated_invoices()
|
||||||
elif self.invoices:
|
elif self.invoices:
|
||||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||||
invoice_details = self.invoices.get(inv)
|
invoice_details = self.invoices.get(inv)
|
||||||
@@ -92,6 +95,57 @@ class Gstr1Report(object):
|
|||||||
row= [key[0], key[1], value[0], value[1]]
|
row= [key[0], key[1], value[0], value[1]]
|
||||||
self.data.append(row)
|
self.data.append(row)
|
||||||
|
|
||||||
|
def get_nil_rated_invoices(self):
|
||||||
|
nil_exempt_output = [
|
||||||
|
{
|
||||||
|
"description": "Inter-State supplies to registered persons",
|
||||||
|
"nil_rated": 0.0,
|
||||||
|
"exempted": 0.0,
|
||||||
|
"non_gst": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Intra-State supplies to registered persons",
|
||||||
|
"nil_rated": 0.0,
|
||||||
|
"exempted": 0.0,
|
||||||
|
"non_gst": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Inter-State supplies to unregistered persons",
|
||||||
|
"nil_rated": 0.0,
|
||||||
|
"exempted": 0.0,
|
||||||
|
"non_gst": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Intra-State supplies to unregistered persons",
|
||||||
|
"nil_rated": 0.0,
|
||||||
|
"exempted": 0.0,
|
||||||
|
"non_gst": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for invoice, details in self.nil_exempt_non_gst.items():
|
||||||
|
invoice_detail = self.invoices.get(invoice)
|
||||||
|
if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"):
|
||||||
|
if is_inter_state(invoice_detail):
|
||||||
|
nil_exempt_output[0]["nil_rated"] += details[0]
|
||||||
|
nil_exempt_output[0]["exempted"] += details[1]
|
||||||
|
nil_exempt_output[0]["non_gst"] += details[2]
|
||||||
|
else:
|
||||||
|
nil_exempt_output[1]["nil_rated"] += details[0]
|
||||||
|
nil_exempt_output[1]["exempted"] += details[1]
|
||||||
|
nil_exempt_output[1]["non_gst"] += details[2]
|
||||||
|
else:
|
||||||
|
if is_inter_state(invoice_detail):
|
||||||
|
nil_exempt_output[2]["nil_rated"] += details[0]
|
||||||
|
nil_exempt_output[2]["exempted"] += details[1]
|
||||||
|
nil_exempt_output[2]["non_gst"] += details[2]
|
||||||
|
else:
|
||||||
|
nil_exempt_output[3]["nil_rated"] += details[0]
|
||||||
|
nil_exempt_output[3]["exempted"] += details[1]
|
||||||
|
nil_exempt_output[3]["non_gst"] += details[2]
|
||||||
|
|
||||||
|
self.data = nil_exempt_output
|
||||||
|
|
||||||
def get_b2c_data(self):
|
def get_b2c_data(self):
|
||||||
b2cs_output = {}
|
b2cs_output = {}
|
||||||
|
|
||||||
@@ -241,10 +295,11 @@ class Gstr1Report(object):
|
|||||||
def get_invoice_items(self):
|
def get_invoice_items(self):
|
||||||
self.invoice_items = frappe._dict()
|
self.invoice_items = frappe._dict()
|
||||||
self.item_tax_rate = frappe._dict()
|
self.item_tax_rate = frappe._dict()
|
||||||
|
self.nil_exempt_non_gst = {}
|
||||||
|
|
||||||
items = frappe.db.sql("""
|
items = frappe.db.sql("""
|
||||||
select item_code, parent, taxable_value, base_net_amount, item_tax_rate
|
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
|
||||||
from `tab%s Item`
|
is_non_gst from `tab%s Item`
|
||||||
where parent in (%s)
|
where parent in (%s)
|
||||||
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
|
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
|
||||||
|
|
||||||
@@ -261,6 +316,16 @@ class Gstr1Report(object):
|
|||||||
tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
|
tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
|
||||||
tax_rate_dict.append(rate)
|
tax_rate_dict.append(rate)
|
||||||
|
|
||||||
|
if d.is_nil_exempt:
|
||||||
|
self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
|
||||||
|
if item_tax_rate:
|
||||||
|
self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0)
|
||||||
|
else:
|
||||||
|
self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0)
|
||||||
|
elif d.is_non_gst:
|
||||||
|
self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
|
||||||
|
self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0)
|
||||||
|
|
||||||
def get_items_based_on_tax_rate(self):
|
def get_items_based_on_tax_rate(self):
|
||||||
self.tax_details = frappe.db.sql("""
|
self.tax_details = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
@@ -323,21 +388,24 @@ class Gstr1Report(object):
|
|||||||
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
||||||
|
|
||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
self.tax_columns = [
|
|
||||||
{
|
|
||||||
"fieldname": "rate",
|
|
||||||
"label": "Rate",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"width": 60
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "taxable_value",
|
|
||||||
"label": "Taxable Value",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"width": 100
|
|
||||||
}
|
|
||||||
]
|
|
||||||
self.other_columns = []
|
self.other_columns = []
|
||||||
|
self.tax_columns = []
|
||||||
|
|
||||||
|
if self.filters.get("type_of_business") != "NIL Rated":
|
||||||
|
self.tax_columns = [
|
||||||
|
{
|
||||||
|
"fieldname": "rate",
|
||||||
|
"label": "Rate",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"width": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxable_value",
|
||||||
|
"label": "Taxable Value",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
if self.filters.get("type_of_business") == "B2B":
|
if self.filters.get("type_of_business") == "B2B":
|
||||||
self.invoice_columns = [
|
self.invoice_columns = [
|
||||||
@@ -706,6 +774,33 @@ class Gstr1Report(object):
|
|||||||
"width": 100
|
"width": 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
elif self.filters.get("type_of_business") == "NIL Rated":
|
||||||
|
self.invoice_columns = [
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"label": "Description",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 420
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "nil_rated",
|
||||||
|
"label": "Nil Rated",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "exempted",
|
||||||
|
"label": "Exempted",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "non_gst",
|
||||||
|
"label": "Non GST",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
self.columns = self.invoice_columns + self.tax_columns + self.other_columns
|
self.columns = self.invoice_columns + self.tax_columns + self.other_columns
|
||||||
|
|
||||||
@@ -769,6 +864,11 @@ def get_json(filters, report_name, data):
|
|||||||
out = get_advances_json(res, gstin)
|
out = get_advances_json(res, gstin)
|
||||||
gst_json["at"] = out
|
gst_json["at"] = out
|
||||||
|
|
||||||
|
elif filters["type_of_business"] == "NIL Rated":
|
||||||
|
res = report_data[:-1]
|
||||||
|
out = get_exempted_json(res)
|
||||||
|
gst_json["nil"] = out
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'report_name': report_name,
|
'report_name': report_name,
|
||||||
'report_type': filters['type_of_business'],
|
'report_type': filters['type_of_business'],
|
||||||
@@ -981,6 +1081,36 @@ def get_cdnr_unreg_json(res, gstin):
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def get_exempted_json(data):
|
||||||
|
out = {
|
||||||
|
"inv": [
|
||||||
|
{
|
||||||
|
"sply_ty": "INTRB2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sply_ty": "INTRAB2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sply_ty": "INTRB2C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sply_ty": "INTRAB2C"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v in enumerate(data):
|
||||||
|
if data[i].get('nil_rated'):
|
||||||
|
out['inv'][i]['nil_amt'] = data[i]['nil_rated']
|
||||||
|
|
||||||
|
if data[i].get('exempted'):
|
||||||
|
out['inv'][i]['expt_amt'] = data[i]['exempted']
|
||||||
|
|
||||||
|
if data[i].get('non_gst'):
|
||||||
|
out['inv'][i]['ngsup_amt'] = data[i]['non_gst']
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
def get_invoice_type_for_cdnr(row):
|
def get_invoice_type_for_cdnr(row):
|
||||||
if row.get('gst_category') == 'SEZ':
|
if row.get('gst_category') == 'SEZ':
|
||||||
if row.get('export_type') == 'WPAY':
|
if row.get('export_type') == 'WPAY':
|
||||||
@@ -1065,3 +1195,9 @@ def download_json_file():
|
|||||||
frappe.response['filecontent'] = data['data']
|
frappe.response['filecontent'] = data['data']
|
||||||
frappe.response['content_type'] = 'application/json'
|
frappe.response['content_type'] = 'application/json'
|
||||||
frappe.response['type'] = 'download'
|
frappe.response['type'] = 'download'
|
||||||
|
|
||||||
|
def is_inter_state(invoice_detail):
|
||||||
|
if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
@@ -20,25 +20,35 @@ def get_columns():
|
|||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
"label": _("Title"),
|
"label": _("Title"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"width": 300
|
"width": 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"label": _("Amount (SAR)"),
|
"label": _("Amount (SAR)"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "adjustment_amount",
|
"fieldname": "adjustment_amount",
|
||||||
"label": _("Adjustment (SAR)"),
|
"label": _("Adjustment (SAR)"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "vat_amount",
|
"fieldname": "vat_amount",
|
||||||
"label": _("VAT Amount (SAR)"),
|
"label": _("VAT Amount (SAR)"),
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
"width": 150,
|
"width": 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"label": _("Currency"),
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 150,
|
||||||
|
"hidden": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -47,6 +57,8 @@ def get_data(filters):
|
|||||||
|
|
||||||
# Validate if vat settings exist
|
# Validate if vat settings exist
|
||||||
company = filters.get('company')
|
company = filters.get('company')
|
||||||
|
company_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||||
|
|
||||||
if frappe.db.exists('KSA VAT Setting', company) is None:
|
if frappe.db.exists('KSA VAT Setting', company) is None:
|
||||||
url = get_url_to_list('KSA VAT Setting')
|
url = get_url_to_list('KSA VAT Setting')
|
||||||
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
|
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
|
||||||
@@ -55,7 +67,7 @@ def get_data(filters):
|
|||||||
ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
|
ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
|
||||||
|
|
||||||
# Sales Heading
|
# Sales Heading
|
||||||
append_data(data, 'VAT on Sales', '', '', '')
|
append_data(data, 'VAT on Sales', '', '', '', company_currency)
|
||||||
|
|
||||||
grand_total_taxable_amount = 0
|
grand_total_taxable_amount = 0
|
||||||
grand_total_taxable_adjustment_amount = 0
|
grand_total_taxable_adjustment_amount = 0
|
||||||
@@ -67,7 +79,7 @@ def get_data(filters):
|
|||||||
|
|
||||||
# Adding results to data
|
# Adding results to data
|
||||||
append_data(data, vat_setting.title, total_taxable_amount,
|
append_data(data, vat_setting.title, total_taxable_amount,
|
||||||
total_taxable_adjustment_amount, total_tax)
|
total_taxable_adjustment_amount, total_tax, company_currency)
|
||||||
|
|
||||||
grand_total_taxable_amount += total_taxable_amount
|
grand_total_taxable_amount += total_taxable_amount
|
||||||
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
|
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
|
||||||
@@ -75,13 +87,13 @@ def get_data(filters):
|
|||||||
|
|
||||||
# Sales Grand Total
|
# Sales Grand Total
|
||||||
append_data(data, 'Grand Total', grand_total_taxable_amount,
|
append_data(data, 'Grand Total', grand_total_taxable_amount,
|
||||||
grand_total_taxable_adjustment_amount, grand_total_tax)
|
grand_total_taxable_adjustment_amount, grand_total_tax, company_currency)
|
||||||
|
|
||||||
# Blank Line
|
# Blank Line
|
||||||
append_data(data, '', '', '', '')
|
append_data(data, '', '', '', '', company_currency)
|
||||||
|
|
||||||
# Purchase Heading
|
# Purchase Heading
|
||||||
append_data(data, 'VAT on Purchases', '', '', '')
|
append_data(data, 'VAT on Purchases', '', '', '', company_currency)
|
||||||
|
|
||||||
grand_total_taxable_amount = 0
|
grand_total_taxable_amount = 0
|
||||||
grand_total_taxable_adjustment_amount = 0
|
grand_total_taxable_adjustment_amount = 0
|
||||||
@@ -93,7 +105,7 @@ def get_data(filters):
|
|||||||
|
|
||||||
# Adding results to data
|
# Adding results to data
|
||||||
append_data(data, vat_setting.title, total_taxable_amount,
|
append_data(data, vat_setting.title, total_taxable_amount,
|
||||||
total_taxable_adjustment_amount, total_tax)
|
total_taxable_adjustment_amount, total_tax, company_currency)
|
||||||
|
|
||||||
grand_total_taxable_amount += total_taxable_amount
|
grand_total_taxable_amount += total_taxable_amount
|
||||||
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
|
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
|
||||||
@@ -101,7 +113,7 @@ def get_data(filters):
|
|||||||
|
|
||||||
# Purchase Grand Total
|
# Purchase Grand Total
|
||||||
append_data(data, 'Grand Total', grand_total_taxable_amount,
|
append_data(data, 'Grand Total', grand_total_taxable_amount,
|
||||||
grand_total_taxable_adjustment_amount, grand_total_tax)
|
grand_total_taxable_adjustment_amount, grand_total_tax, company_currency)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -147,9 +159,10 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def append_data(data, title, amount, adjustment_amount, vat_amount):
|
def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency):
|
||||||
"""Returns data with appended value."""
|
"""Returns data with appended value."""
|
||||||
data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
|
data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount,
|
||||||
|
"currency": company_currency})
|
||||||
|
|
||||||
def get_tax_amount(item_code, account_head, doctype, parent):
|
def get_tax_amount(item_code, account_head, doctype, parent):
|
||||||
if doctype == 'Sales Invoice':
|
if doctype == 'Sales Invoice':
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
|
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
|
||||||
})
|
})
|
||||||
} else if (available_qty < qty_needed) {
|
} else if (available_qty < qty_needed) {
|
||||||
frappe.show_alert({
|
frappe.throw({
|
||||||
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
|
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
|
||||||
indicator: 'orange'
|
indicator: 'orange'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
`<div class="item-wrapper"
|
`<div class="item-wrapper"
|
||||||
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
|
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
|
||||||
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
|
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
|
||||||
data-rate="${escape(price_list_rate)}"
|
data-rate="${escape(price_list_rate || 0)}"
|
||||||
title="${item.item_name}">
|
title="${item.item_name}">
|
||||||
|
|
||||||
${get_item_image_html()}
|
${get_item_image_html()}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ def get_data(conditions, filters):
|
|||||||
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
||||||
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
||||||
soi.warehouse as warehouse,
|
soi.warehouse as warehouse,
|
||||||
so.company, soi.name
|
so.company, soi.name,
|
||||||
|
soi.description as description
|
||||||
FROM
|
FROM
|
||||||
`tabSales Order` so,
|
`tabSales Order` so,
|
||||||
`tabSales Order Item` soi
|
`tabSales Order Item` soi
|
||||||
@@ -179,6 +180,12 @@ def get_columns(filters):
|
|||||||
"options": "Item",
|
"options": "Item",
|
||||||
"width": 100
|
"width": 100
|
||||||
})
|
})
|
||||||
|
columns.append({
|
||||||
|
"label":_("Description"),
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"width": 100
|
||||||
|
})
|
||||||
|
|
||||||
columns.extend([
|
columns.extend([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,14 +79,11 @@ frappe.ui.form.on("Company", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if(!frm.doc.__islocal) {
|
frm.toggle_display('address_html', !frm.is_new());
|
||||||
frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
|
|
||||||
frm.set_df_property("parent_company", "read_only", 1);
|
|
||||||
disbale_coa_fields(frm);
|
|
||||||
}
|
|
||||||
|
|
||||||
frm.toggle_display('address_html', !frm.doc.__islocal);
|
if (!frm.is_new()) {
|
||||||
if(!frm.doc.__islocal) {
|
frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
|
||||||
|
disbale_coa_fields(frm);
|
||||||
frappe.contacts.render_address_and_contact(frm);
|
frappe.contacts.render_address_and_contact(frm);
|
||||||
|
|
||||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
|
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
|
||||||
@@ -216,6 +213,9 @@ erpnext.company.setup_queries = function(frm) {
|
|||||||
["default_payroll_payable_account", {"root_type": "Liability"}],
|
["default_payroll_payable_account", {"root_type": "Liability"}],
|
||||||
["round_off_account", {"root_type": "Expense"}],
|
["round_off_account", {"root_type": "Expense"}],
|
||||||
["write_off_account", {"root_type": "Expense"}],
|
["write_off_account", {"root_type": "Expense"}],
|
||||||
|
["default_deferred_expense_account", {}],
|
||||||
|
["default_deferred_revenue_account", {}],
|
||||||
|
["default_expense_claim_payable_account", {}],
|
||||||
["default_discount_account", {}],
|
["default_discount_account", {}],
|
||||||
["discount_allowed_account", {"root_type": "Expense"}],
|
["discount_allowed_account", {"root_type": "Expense"}],
|
||||||
["discount_received_account", {"root_type": "Income"}],
|
["discount_received_account", {"root_type": "Income"}],
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class Company(NestedSet):
|
|||||||
self.validate_perpetual_inventory()
|
self.validate_perpetual_inventory()
|
||||||
self.validate_perpetual_inventory_for_non_stock_items()
|
self.validate_perpetual_inventory_for_non_stock_items()
|
||||||
self.check_country_change()
|
self.check_country_change()
|
||||||
|
self.check_parent_changed()
|
||||||
self.set_chart_of_accounts()
|
self.set_chart_of_accounts()
|
||||||
self.validate_parent_company()
|
self.validate_parent_company()
|
||||||
|
|
||||||
@@ -132,6 +133,10 @@ class Company(NestedSet):
|
|||||||
self.name in frappe.local.enable_perpetual_inventory:
|
self.name in frappe.local.enable_perpetual_inventory:
|
||||||
frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory
|
frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory
|
||||||
|
|
||||||
|
if frappe.flags.parent_company_changed:
|
||||||
|
from frappe.utils.nestedset import rebuild_tree
|
||||||
|
rebuild_tree("Company", "parent_company")
|
||||||
|
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
def create_default_warehouses(self):
|
def create_default_warehouses(self):
|
||||||
@@ -193,7 +198,7 @@ class Company(NestedSet):
|
|||||||
def check_country_change(self):
|
def check_country_change(self):
|
||||||
frappe.flags.country_change = False
|
frappe.flags.country_change = False
|
||||||
|
|
||||||
if not self.get('__islocal') and \
|
if not self.is_new() and \
|
||||||
self.country != frappe.get_cached_value('Company', self.name, 'country'):
|
self.country != frappe.get_cached_value('Company', self.name, 'country'):
|
||||||
frappe.flags.country_change = True
|
frappe.flags.country_change = True
|
||||||
|
|
||||||
@@ -398,6 +403,13 @@ class Company(NestedSet):
|
|||||||
if not frappe.db.get_value('GL Entry', {'company': self.name}):
|
if not frappe.db.get_value('GL Entry', {'company': self.name}):
|
||||||
frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name)
|
frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name)
|
||||||
|
|
||||||
|
def check_parent_changed(self):
|
||||||
|
frappe.flags.parent_company_changed = False
|
||||||
|
|
||||||
|
if not self.is_new() and \
|
||||||
|
self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"):
|
||||||
|
frappe.flags.parent_company_changed = True
|
||||||
|
|
||||||
def get_name_with_abbr(name, company):
|
def get_name_with_abbr(name, company):
|
||||||
company_abbr = frappe.get_cached_value('Company', company, "abbr")
|
company_abbr = frappe.get_cached_value('Company', company, "abbr")
|
||||||
parts = name.split(" - ")
|
parts = name.split(" - ")
|
||||||
|
|||||||
@@ -93,6 +93,61 @@ class TestCompany(unittest.TestCase):
|
|||||||
frappe.db.sql(""" delete from `tabMode of Payment Account`
|
frappe.db.sql(""" delete from `tabMode of Payment Account`
|
||||||
where company =%s """, (company))
|
where company =%s """, (company))
|
||||||
|
|
||||||
|
def test_basic_tree(self, records=None):
|
||||||
|
min_lft = 1
|
||||||
|
max_rgt = frappe.db.sql("select max(rgt) from `tabCompany`")[0][0]
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
records = test_records[2:]
|
||||||
|
|
||||||
|
for company in records:
|
||||||
|
lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"],
|
||||||
|
["lft", "rgt", "parent_company"])
|
||||||
|
|
||||||
|
if parent_company:
|
||||||
|
parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company,
|
||||||
|
["lft", "rgt"])
|
||||||
|
else:
|
||||||
|
# root
|
||||||
|
parent_lft = min_lft - 1
|
||||||
|
parent_rgt = max_rgt + 1
|
||||||
|
|
||||||
|
self.assertTrue(lft)
|
||||||
|
self.assertTrue(rgt)
|
||||||
|
self.assertTrue(lft < rgt)
|
||||||
|
self.assertTrue(parent_lft < parent_rgt)
|
||||||
|
self.assertTrue(lft > parent_lft)
|
||||||
|
self.assertTrue(rgt < parent_rgt)
|
||||||
|
self.assertTrue(lft >= min_lft)
|
||||||
|
self.assertTrue(rgt <= max_rgt)
|
||||||
|
|
||||||
|
def get_no_of_children(self, company):
|
||||||
|
def get_no_of_children(companies, no_of_children):
|
||||||
|
children = []
|
||||||
|
for company in companies:
|
||||||
|
children += frappe.db.sql_list("""select name from `tabCompany`
|
||||||
|
where ifnull(parent_company, '')=%s""", company or '')
|
||||||
|
|
||||||
|
if len(children):
|
||||||
|
return get_no_of_children(children, no_of_children + len(children))
|
||||||
|
else:
|
||||||
|
return no_of_children
|
||||||
|
|
||||||
|
return get_no_of_children([company], 0)
|
||||||
|
|
||||||
|
def test_change_parent_company(self):
|
||||||
|
child_company = frappe.get_doc("Company", "_Test Company 5")
|
||||||
|
|
||||||
|
# changing parent of company
|
||||||
|
child_company.parent_company = "_Test Company 3"
|
||||||
|
child_company.save()
|
||||||
|
self.test_basic_tree()
|
||||||
|
|
||||||
|
# move it back
|
||||||
|
child_company.parent_company = "_Test Company 4"
|
||||||
|
child_company.save()
|
||||||
|
self.test_basic_tree()
|
||||||
|
|
||||||
def create_company_communication(doctype, docname):
|
def create_company_communication(doctype, docname):
|
||||||
comm = frappe.get_doc({
|
comm = frappe.get_doc({
|
||||||
"doctype": "Communication",
|
"doctype": "Communication",
|
||||||
|
|||||||
@@ -350,7 +350,8 @@ def add_uom_data():
|
|||||||
"doctype": "UOM",
|
"doctype": "UOM",
|
||||||
"uom_name": _(d.get("uom_name")),
|
"uom_name": _(d.get("uom_name")),
|
||||||
"name": _(d.get("uom_name")),
|
"name": _(d.get("uom_name")),
|
||||||
"must_be_whole_number": d.get("must_be_whole_number")
|
"must_be_whole_number": d.get("must_be_whole_number"),
|
||||||
|
"enabled": 1,
|
||||||
}).db_insert()
|
}).db_insert()
|
||||||
|
|
||||||
# bootstrap uom conversion factors
|
# bootstrap uom conversion factors
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
|||||||
join `tabStock Ledger Entry` ignore index (item_code, warehouse)
|
join `tabStock Ledger Entry` ignore index (item_code, warehouse)
|
||||||
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
|
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
|
||||||
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
|
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
|
||||||
|
and `tabStock Ledger Entry`.is_cancelled = 0
|
||||||
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
|
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
|
||||||
group by batch_id
|
group by batch_id
|
||||||
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
|
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
|
||||||
@@ -313,3 +314,30 @@ def make_batch(args):
|
|||||||
if frappe.db.get_value("Item", args.item, "has_batch_no"):
|
if frappe.db.get_value("Item", args.item, "has_batch_no"):
|
||||||
args.doctype = "Batch"
|
args.doctype = "Batch"
|
||||||
frappe.get_doc(args).insert().name
|
frappe.get_doc(args).insert().name
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_pos_reserved_batch_qty(filters):
|
||||||
|
import json
|
||||||
|
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
|
if isinstance(filters, str):
|
||||||
|
filters = json.loads(filters)
|
||||||
|
|
||||||
|
p = frappe.qb.DocType("POS Invoice").as_("p")
|
||||||
|
item = frappe.qb.DocType("POS Invoice Item").as_("item")
|
||||||
|
sum_qty = Sum(item.qty).as_("qty")
|
||||||
|
|
||||||
|
reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where(
|
||||||
|
(p.name == item.parent) &
|
||||||
|
(p.consolidated_invoice.isnull()) &
|
||||||
|
(p.status != "Consolidated") &
|
||||||
|
(p.docstatus == 1) &
|
||||||
|
(item.docstatus == 1) &
|
||||||
|
(item.item_code == filters.get('item_code')) &
|
||||||
|
(item.warehouse == filters.get('warehouse')) &
|
||||||
|
(item.batch_no == filters.get('batch_no'))
|
||||||
|
).run()
|
||||||
|
|
||||||
|
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
|
||||||
|
return flt_reserved_batch_qty
|
||||||
|
|||||||
@@ -421,10 +421,16 @@ def update_serial_nos(sle, item_det):
|
|||||||
def get_auto_serial_nos(serial_no_series, qty):
|
def get_auto_serial_nos(serial_no_series, qty):
|
||||||
serial_nos = []
|
serial_nos = []
|
||||||
for i in range(cint(qty)):
|
for i in range(cint(qty)):
|
||||||
serial_nos.append(make_autoname(serial_no_series, "Serial No"))
|
serial_nos.append(get_new_serial_number(serial_no_series))
|
||||||
|
|
||||||
return "\n".join(serial_nos)
|
return "\n".join(serial_nos)
|
||||||
|
|
||||||
|
def get_new_serial_number(series):
|
||||||
|
sr_no = make_autoname(series, "Serial No")
|
||||||
|
if frappe.db.exists("Serial No", sr_no):
|
||||||
|
sr_no = get_new_serial_number(series)
|
||||||
|
return sr_no
|
||||||
|
|
||||||
def auto_make_serial_nos(args):
|
def auto_make_serial_nos(args):
|
||||||
serial_nos = get_serial_nos(args.get('serial_no'))
|
serial_nos = get_serial_nos(args.get('serial_no'))
|
||||||
created_numbers = []
|
created_numbers = []
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
@@ -21,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase
|
|||||||
|
|
||||||
|
|
||||||
class TestSerialNo(ERPNextTestCase):
|
class TestSerialNo(ERPNextTestCase):
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_cannot_create_direct(self):
|
def test_cannot_create_direct(self):
|
||||||
frappe.delete_doc_if_exists("Serial No", "_TCSER0001")
|
frappe.delete_doc_if_exists("Serial No", "_TCSER0001")
|
||||||
|
|
||||||
@@ -176,6 +182,24 @@ class TestSerialNo(ERPNextTestCase):
|
|||||||
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
|
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
|
||||||
self.assertEqual(sn_doc.purchase_document_no, se.name)
|
self.assertEqual(sn_doc.purchase_document_no, se.name)
|
||||||
|
|
||||||
|
def test_auto_creation_of_serial_no(self):
|
||||||
|
"""
|
||||||
|
Test if auto created Serial No excludes existing serial numbers
|
||||||
|
"""
|
||||||
|
item_code = make_item("_Test Auto Serial Item ", {
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "XYZ.###"
|
||||||
|
}).item_code
|
||||||
|
|
||||||
|
# Reserve XYZ005
|
||||||
|
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005")
|
||||||
|
# XYZ005 is already used and will throw an error if used again
|
||||||
|
pr_2 = make_purchase_receipt(item_code=item_code, qty=10)
|
||||||
|
|
||||||
|
self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005")
|
||||||
|
for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no):
|
||||||
|
self.assertNotEqual(serial_no, "XYZ005")
|
||||||
|
|
||||||
def test_serial_no_sanitation(self):
|
def test_serial_no_sanitation(self):
|
||||||
"Test if Serial No input is sanitised before entering the DB."
|
"Test if Serial No input is sanitised before entering the DB."
|
||||||
item_code = "_Test Serialized Item"
|
item_code = "_Test Serialized Item"
|
||||||
@@ -192,7 +216,28 @@ class TestSerialNo(ERPNextTestCase):
|
|||||||
|
|
||||||
self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021")
|
self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021")
|
||||||
|
|
||||||
frappe.db.rollback()
|
def test_correct_serial_no_incoming_rate(self):
|
||||||
|
""" Check correct consumption rate based on serial no record.
|
||||||
|
"""
|
||||||
|
item_code = "_Test Serialized Item"
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
serial_nos = ["LOWVALUATION", "HIGHVALUATION"]
|
||||||
|
|
||||||
|
in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42,
|
||||||
|
serial_no=serial_nos[0])
|
||||||
|
in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113,
|
||||||
|
serial_no=serial_nos[1])
|
||||||
|
|
||||||
|
out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True)
|
||||||
|
|
||||||
|
# change serial no
|
||||||
|
out.items[0].serial_no = serial_nos[1]
|
||||||
|
out.save()
|
||||||
|
out.submit()
|
||||||
|
|
||||||
|
value_diff = frappe.db.get_value("Stock Ledger Entry",
|
||||||
|
{"voucher_no": out.name, "voucher_type": "Delivery Note"},
|
||||||
|
"stock_value_difference"
|
||||||
|
)
|
||||||
|
self.assertEqual(value_diff, -113)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.rollback()
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from collections import defaultdict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
||||||
from six import iteritems, itervalues, string_types
|
from six import iteritems, itervalues, string_types
|
||||||
|
|
||||||
@@ -86,8 +87,11 @@ class StockEntry(StockController):
|
|||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
self.validate_work_order()
|
self.validate_work_order()
|
||||||
self.validate_bom()
|
self.validate_bom()
|
||||||
self.mark_finished_and_scrap_items()
|
|
||||||
self.validate_finished_goods()
|
if self.purpose in ("Manufacture", "Repack"):
|
||||||
|
self.mark_finished_and_scrap_items()
|
||||||
|
self.validate_finished_goods()
|
||||||
|
|
||||||
self.validate_with_material_request()
|
self.validate_with_material_request()
|
||||||
self.validate_batch()
|
self.validate_batch()
|
||||||
self.validate_inspection()
|
self.validate_inspection()
|
||||||
@@ -110,8 +114,12 @@ class StockEntry(StockController):
|
|||||||
self.set_actual_qty()
|
self.set_actual_qty()
|
||||||
self.calculate_rate_and_amount()
|
self.calculate_rate_and_amount()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
|
||||||
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
if not self.get("purpose") == "Manufacture":
|
||||||
|
# ignore scrap item wh difference and empty source/target wh
|
||||||
|
# in Manufacture Entry
|
||||||
|
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||||
|
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
@@ -702,26 +710,25 @@ class StockEntry(StockController):
|
|||||||
validate_bom_no(item_code, d.bom_no)
|
validate_bom_no(item_code, d.bom_no)
|
||||||
|
|
||||||
def mark_finished_and_scrap_items(self):
|
def mark_finished_and_scrap_items(self):
|
||||||
if self.purpose in ("Repack", "Manufacture"):
|
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
||||||
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
return
|
||||||
return
|
|
||||||
|
|
||||||
finished_item = self.get_finished_item()
|
finished_item = self.get_finished_item()
|
||||||
|
|
||||||
if not finished_item and self.purpose == "Manufacture":
|
if not finished_item and self.purpose == "Manufacture":
|
||||||
# In case of independent Manufacture entry, don't auto set
|
# In case of independent Manufacture entry, don't auto set
|
||||||
# user must decide and set
|
# user must decide and set
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.t_warehouse and not d.s_warehouse:
|
if d.t_warehouse and not d.s_warehouse:
|
||||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||||
d.is_finished_item = 1
|
d.is_finished_item = 1
|
||||||
else:
|
|
||||||
d.is_scrap_item = 1
|
|
||||||
else:
|
else:
|
||||||
d.is_finished_item = 0
|
d.is_scrap_item = 1
|
||||||
d.is_scrap_item = 0
|
else:
|
||||||
|
d.is_finished_item = 0
|
||||||
|
d.is_scrap_item = 0
|
||||||
|
|
||||||
def get_finished_item(self):
|
def get_finished_item(self):
|
||||||
finished_item = None
|
finished_item = None
|
||||||
@@ -734,9 +741,9 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def validate_finished_goods(self):
|
def validate_finished_goods(self):
|
||||||
"""
|
"""
|
||||||
1. Check if FG exists
|
1. Check if FG exists (mfg, repack)
|
||||||
2. Check if Multiple FG Items are present
|
2. Check if Multiple FG Items are present (mfg)
|
||||||
3. Check FG Item and Qty against WO if present
|
3. Check FG Item and Qty against WO if present (mfg)
|
||||||
"""
|
"""
|
||||||
production_item, wo_qty, finished_items = None, 0, []
|
production_item, wo_qty, finished_items = None, 0, []
|
||||||
|
|
||||||
@@ -749,8 +756,9 @@ class StockEntry(StockController):
|
|||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
if d.is_finished_item:
|
if d.is_finished_item:
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
|
# Independent MFG Entry/ Repack Entry, no WO to match against
|
||||||
finished_items.append(d.item_code)
|
finished_items.append(d.item_code)
|
||||||
continue # Independent Manufacture Entry, no WO to match against
|
continue
|
||||||
|
|
||||||
if d.item_code != production_item:
|
if d.item_code != production_item:
|
||||||
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
|
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
|
||||||
@@ -763,19 +771,17 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
finished_items.append(d.item_code)
|
finished_items.append(d.item_code)
|
||||||
|
|
||||||
if len(set(finished_items)) > 1:
|
if not finished_items:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
msg=_("Multiple items cannot be marked as finished item"),
|
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||||
title=_("Note"),
|
title=_("Missing Finished Good"), exc=FinishedGoodError
|
||||||
exc=FinishedGoodError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.purpose == "Manufacture":
|
if self.purpose == "Manufacture":
|
||||||
if not finished_items:
|
if len(set(finished_items)) > 1:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
msg=_("Multiple items cannot be marked as finished item"),
|
||||||
title=_("Missing Finished Good"),
|
title=_("Note"), exc=FinishedGoodError
|
||||||
exc=FinishedGoodError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
allowance_percentage = flt(
|
allowance_percentage = flt(
|
||||||
@@ -1276,22 +1282,29 @@ class StockEntry(StockController):
|
|||||||
if not self.pro_doc:
|
if not self.pro_doc:
|
||||||
self.set_work_order_details()
|
self.set_work_order_details()
|
||||||
|
|
||||||
scrap_items = frappe.db.sql('''
|
if not self.pro_doc.operations:
|
||||||
SELECT
|
|
||||||
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
|
|
||||||
FROM
|
|
||||||
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
|
|
||||||
WHERE
|
|
||||||
JCSI.parent = JC.name AND JC.docstatus = 1
|
|
||||||
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
|
|
||||||
GROUP BY
|
|
||||||
JCSI.item_code
|
|
||||||
''', self.work_order, as_dict=1)
|
|
||||||
|
|
||||||
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
|
|
||||||
if pending_qty <=0:
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
job_card = frappe.qb.DocType('Job Card')
|
||||||
|
job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item')
|
||||||
|
|
||||||
|
scrap_items = (
|
||||||
|
frappe.qb.from_(job_card)
|
||||||
|
.select(
|
||||||
|
Sum(job_card_scrap_item.stock_qty).as_('stock_qty'),
|
||||||
|
job_card_scrap_item.item_code, job_card_scrap_item.item_name,
|
||||||
|
job_card_scrap_item.description, job_card_scrap_item.stock_uom)
|
||||||
|
.join(job_card_scrap_item)
|
||||||
|
.on(job_card_scrap_item.parent == job_card.name)
|
||||||
|
.where(
|
||||||
|
(job_card_scrap_item.item_code.isnotnull())
|
||||||
|
& (job_card.work_order == self.work_order)
|
||||||
|
& (job_card.docstatus == 1))
|
||||||
|
.groupby(job_card_scrap_item.item_code)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
|
pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty)
|
||||||
|
|
||||||
used_scrap_items = self.get_used_scrap_items()
|
used_scrap_items = self.get_used_scrap_items()
|
||||||
for row in scrap_items:
|
for row in scrap_items:
|
||||||
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||||
@@ -1305,6 +1318,9 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
return scrap_items
|
return scrap_items
|
||||||
|
|
||||||
|
def get_completed_job_card_qty(self):
|
||||||
|
return flt(min([d.completed_qty for d in self.pro_doc.operations]))
|
||||||
|
|
||||||
def get_used_scrap_items(self):
|
def get_used_scrap_items(self):
|
||||||
used_scrap_items = defaultdict(float)
|
used_scrap_items = defaultdict(float)
|
||||||
data = frappe.get_all(
|
data = frappe.get_all(
|
||||||
|
|||||||
@@ -227,9 +227,47 @@ class TestStockEntry(ERPNextTestCase):
|
|||||||
|
|
||||||
mtn.cancel()
|
mtn.cancel()
|
||||||
|
|
||||||
def test_repack_no_change_in_valuation(self):
|
def test_repack_multiple_fg(self):
|
||||||
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
|
"Test `is_finished_item` for one item repacked into two items."
|
||||||
|
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
|
||||||
|
|
||||||
|
repack = frappe.copy_doc(test_records[3])
|
||||||
|
repack.posting_date = nowdate()
|
||||||
|
repack.posting_time = nowtime()
|
||||||
|
|
||||||
|
repack.items[0].qty = 100.0
|
||||||
|
repack.items[0].transfer_qty = 100.0
|
||||||
|
repack.items[1].qty = 50.0
|
||||||
|
|
||||||
|
repack.append("items", {
|
||||||
|
"conversion_factor": 1.0,
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"doctype": "Stock Entry Detail",
|
||||||
|
"expense_account": "Stock Adjustment - _TC",
|
||||||
|
"basic_rate": 150,
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"parentfield": "items",
|
||||||
|
"qty": 50.0,
|
||||||
|
"stock_uom": "_Test UOM",
|
||||||
|
"t_warehouse": "_Test Warehouse - _TC",
|
||||||
|
"transfer_qty": 50.0,
|
||||||
|
"uom": "_Test UOM"
|
||||||
|
})
|
||||||
|
repack.set_stock_entry_type()
|
||||||
|
repack.insert()
|
||||||
|
|
||||||
|
self.assertEqual(repack.items[1].is_finished_item, 1)
|
||||||
|
self.assertEqual(repack.items[2].is_finished_item, 1)
|
||||||
|
|
||||||
|
repack.items[1].is_finished_item = 0
|
||||||
|
repack.items[2].is_finished_item = 0
|
||||||
|
|
||||||
|
# must raise error if 0 fg in repack entry
|
||||||
|
self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
|
||||||
|
|
||||||
|
repack.delete() # teardown
|
||||||
|
|
||||||
|
def test_repack_no_change_in_valuation(self):
|
||||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
|
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
|
||||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
|
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
|
||||||
qty=50, basic_rate=100)
|
qty=50, basic_rate=100)
|
||||||
@@ -814,6 +852,34 @@ class TestStockEntry(ERPNextTestCase):
|
|||||||
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
|
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
|
||||||
self.assertEqual(se.get("items")[0].amount, 0)
|
self.assertEqual(se.get("items")[0].amount, 0)
|
||||||
|
|
||||||
|
def test_zero_incoming_rate(self):
|
||||||
|
""" Make sure incoming rate of 0 is allowed while consuming.
|
||||||
|
|
||||||
|
qty | rate | valuation rate
|
||||||
|
1 | 100 | 100
|
||||||
|
1 | 0 | 50
|
||||||
|
-1 | 100 | 0
|
||||||
|
-1 | 0 <--- assert this
|
||||||
|
"""
|
||||||
|
item_code = "_TestZeroVal"
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
create_item('_TestZeroVal')
|
||||||
|
_receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100)
|
||||||
|
receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True)
|
||||||
|
receipt2.items[0].allow_zero_valuation_rate = 1
|
||||||
|
receipt2.save()
|
||||||
|
receipt2.submit()
|
||||||
|
|
||||||
|
issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
|
||||||
|
|
||||||
|
value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
|
||||||
|
self.assertEqual(value_diff, -100)
|
||||||
|
|
||||||
|
issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
|
||||||
|
value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
|
||||||
|
self.assertEqual(value_diff, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_gle_for_opening_stock_entry(self):
|
def test_gle_for_opening_stock_entry(self):
|
||||||
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
|
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
|
||||||
company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
|
company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters):
|
|||||||
return frappe.db.sql("""select item_code, batch_no, warehouse,
|
return frappe.db.sql("""select item_code, batch_no, warehouse,
|
||||||
posting_date, actual_qty
|
posting_date, actual_qty
|
||||||
from `tabStock Ledger Entry`
|
from `tabStock Ledger Entry`
|
||||||
where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
|
where is_cancelled = 0
|
||||||
|
and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
|
||||||
conditions, as_dict=1)
|
conditions, as_dict=1)
|
||||||
|
|
||||||
def get_item_warehouse_batch_map(filters, float_precision):
|
def get_item_warehouse_batch_map(filters, float_precision):
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis
|
|||||||
voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
|
voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
|
||||||
svd_list = frappe.get_list(
|
svd_list = frappe.get_list(
|
||||||
'Stock Ledger Entry', fields=['item_code','stock_value_difference'],
|
'Stock Ledger Entry', fields=['item_code','stock_value_difference'],
|
||||||
filters=[('voucher_no', 'in', voucher_nos)]
|
filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)]
|
||||||
)
|
)
|
||||||
assign_item_groups_to_svd_list(svd_list)
|
assign_item_groups_to_svd_list(svd_list)
|
||||||
return svd_list
|
return svd_list
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ def get_stock_ledger_entries(report_filters):
|
|||||||
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
|
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
|
||||||
'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
|
'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
|
||||||
|
|
||||||
filters = {'serial_no': ("is", "set")}
|
filters = {'serial_no': ("is", "set"), "is_cancelled": 0}
|
||||||
|
|
||||||
if report_filters.get('item_code'):
|
if report_filters.get('item_code'):
|
||||||
filters['item_code'] = report_filters.get('item_code')
|
filters['item_code'] = report_filters.get('item_code')
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ def get_consumed_items(condition):
|
|||||||
on sle.voucher_no = se.name
|
on sle.voucher_no = se.name
|
||||||
where
|
where
|
||||||
actual_qty < 0
|
actual_qty < 0
|
||||||
|
and is_cancelled = 0
|
||||||
and voucher_type not in ('Delivery Note', 'Sales Invoice')
|
and voucher_type not in ('Delivery Note', 'Sales Invoice')
|
||||||
%s
|
%s
|
||||||
group by item_code""" % condition, as_dict=1)
|
group by item_code""" % condition, as_dict=1)
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ def get_args_for_future_sle(row):
|
|||||||
|
|
||||||
def validate_serial_no(sle):
|
def validate_serial_no(sle):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
for sn in get_serial_nos(sle.serial_no):
|
for sn in get_serial_nos(sle.serial_no):
|
||||||
args = copy.deepcopy(sle)
|
args = copy.deepcopy(sle)
|
||||||
args.serial_no = sn
|
args.serial_no = sn
|
||||||
@@ -415,6 +416,8 @@ class update_entries_after(object):
|
|||||||
return sorted(entries_to_fix, key=lambda k: k['timestamp'])
|
return sorted(entries_to_fix, key=lambda k: k['timestamp'])
|
||||||
|
|
||||||
def process_sle(self, sle):
|
def process_sle(self, sle):
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
# previous sle data for this warehouse
|
# previous sle data for this warehouse
|
||||||
self.wh_data = self.data[sle.warehouse]
|
self.wh_data = self.data[sle.warehouse]
|
||||||
|
|
||||||
@@ -429,7 +432,7 @@ class update_entries_after(object):
|
|||||||
if not self.args.get("sle_id"):
|
if not self.args.get("sle_id"):
|
||||||
self.get_dynamic_incoming_outgoing_rate(sle)
|
self.get_dynamic_incoming_outgoing_rate(sle)
|
||||||
|
|
||||||
if sle.serial_no:
|
if get_serial_nos(sle.serial_no):
|
||||||
self.get_serialized_values(sle)
|
self.get_serialized_values(sle)
|
||||||
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
|
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
|
||||||
if sle.voucher_type == "Stock Reconciliation":
|
if sle.voucher_type == "Stock Reconciliation":
|
||||||
@@ -441,8 +444,9 @@ class update_entries_after(object):
|
|||||||
# assert
|
# assert
|
||||||
self.wh_data.valuation_rate = sle.valuation_rate
|
self.wh_data.valuation_rate = sle.valuation_rate
|
||||||
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
||||||
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
|
|
||||||
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
|
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
|
||||||
|
if self.valuation_method != "Moving Average":
|
||||||
|
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
|
||||||
else:
|
else:
|
||||||
if self.valuation_method == "Moving Average":
|
if self.valuation_method == "Moving Average":
|
||||||
self.get_moving_average_values(sle)
|
self.get_moving_average_values(sle)
|
||||||
@@ -595,9 +599,9 @@ class update_entries_after(object):
|
|||||||
incoming_rate = self.wh_data.valuation_rate
|
incoming_rate = self.wh_data.valuation_rate
|
||||||
|
|
||||||
stock_value_change = 0
|
stock_value_change = 0
|
||||||
if incoming_rate:
|
if actual_qty > 0:
|
||||||
stock_value_change = actual_qty * incoming_rate
|
stock_value_change = actual_qty * incoming_rate
|
||||||
elif actual_qty < 0:
|
else:
|
||||||
# In case of delivery/stock issue, get average purchase rate
|
# In case of delivery/stock issue, get average purchase rate
|
||||||
# of serial nos of current entry
|
# of serial nos of current entry
|
||||||
if not sle.is_cancelled:
|
if not sle.is_cancelled:
|
||||||
@@ -639,6 +643,7 @@ class update_entries_after(object):
|
|||||||
where
|
where
|
||||||
company = %s
|
company = %s
|
||||||
and actual_qty > 0
|
and actual_qty > 0
|
||||||
|
and is_cancelled = 0
|
||||||
and (serial_no = %s
|
and (serial_no = %s
|
||||||
or serial_no like %s
|
or serial_no like %s
|
||||||
or serial_no like %s
|
or serial_no like %s
|
||||||
@@ -942,6 +947,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
|
|||||||
item_code = %s
|
item_code = %s
|
||||||
AND warehouse = %s
|
AND warehouse = %s
|
||||||
AND valuation_rate >= 0
|
AND valuation_rate >= 0
|
||||||
|
AND is_cancelled = 0
|
||||||
AND NOT (voucher_no = %s AND voucher_type = %s)
|
AND NOT (voucher_no = %s AND voucher_type = %s)
|
||||||
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
|
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
|
||||||
|
|
||||||
@@ -952,6 +958,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
|
|||||||
where
|
where
|
||||||
item_code = %s
|
item_code = %s
|
||||||
AND valuation_rate > 0
|
AND valuation_rate > 0
|
||||||
|
AND is_cancelled = 0
|
||||||
AND NOT(voucher_no = %s AND voucher_type = %s)
|
AND NOT(voucher_no = %s AND voucher_type = %s)
|
||||||
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
|
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
|
|||||||
|
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
|
|
||||||
if not posting_date: posting_date = nowdate()
|
if posting_date is None: posting_date = nowdate()
|
||||||
if not posting_time: posting_time = nowtime()
|
if posting_time is None: posting_time = nowtime()
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ class TransactionBase(StatusUpdater):
|
|||||||
|
|
||||||
if len(child_table_values) > 1:
|
if len(child_table_values) > 1:
|
||||||
self.set(default_field, None)
|
self.set(default_field, None)
|
||||||
else:
|
|
||||||
self.set(default_field, list(child_table_values)[0])
|
|
||||||
|
|
||||||
def delete_events(ref_type, ref_name):
|
def delete_events(ref_type, ref_name):
|
||||||
events = frappe.db.sql_list(""" SELECT
|
events = frappe.db.sql_list(""" SELECT
|
||||||
|
|||||||
Reference in New Issue
Block a user