mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-18 00:55:02 +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" \
|
||||
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):
|
||||
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
||||
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":
|
||||
against, project = doc.customer, doc.project
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
@@ -407,8 +413,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
'account': credit_account,
|
||||
'credit': base_amount,
|
||||
'credit_in_account_currency': amount,
|
||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
||||
'party': against,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
@@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
'account': debit_account,
|
||||
'debit': base_amount,
|
||||
'debit_in_account_currency': amount,
|
||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
||||
'party': against,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
|
||||
@@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
|
||||
frm.trigger('add_toolbar_buttons');
|
||||
}
|
||||
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.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) {
|
||||
frm.add_custom_button(__('Chart of Accounts'),
|
||||
function () { frappe.set_route("Tree", "Account"); });
|
||||
frm.add_custom_button(__('Chart of Accounts'), () => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
}, __('View'));
|
||||
|
||||
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({
|
||||
doc: frm.doc,
|
||||
method: 'convert_group_to_ledger',
|
||||
@@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, __('Actions'));
|
||||
|
||||
} else if (cint(frm.doc.is_group) == 0
|
||||
&& 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 = {
|
||||
"account": frm.doc.name,
|
||||
"from_date": frappe.sys_defaults.year_start_date,
|
||||
@@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
|
||||
"company": frm.doc.company
|
||||
};
|
||||
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({
|
||||
doc: frm.doc,
|
||||
method: 'convert_ledger_to_group',
|
||||
@@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, __('Actions'));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: ["in", frm.doc.company],
|
||||
company: frm.doc.company,
|
||||
'is_company_account': 1
|
||||
},
|
||||
};
|
||||
|
||||
@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
"withdrawal",
|
||||
"description",
|
||||
"reference_number",
|
||||
"bank_account"
|
||||
"bank_account",
|
||||
"currency"
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
if(frm.doc.docstatus==1) {
|
||||
frm.add_custom_button(__('Reverse Journal Entry'), function() {
|
||||
return erpnext.journal_entry.reverse_journal_entry(frm);
|
||||
}, __('Make'));
|
||||
}, __('Actions'));
|
||||
}
|
||||
|
||||
if (frm.doc.__islocal) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"reversal_of",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
@@ -515,13 +516,21 @@
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-09 15:31:14.484029",
|
||||
"modified": "2022-01-04 13:39:36.485954",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -397,13 +397,14 @@ class JournalEntry(AccountsController):
|
||||
debit_or_credit = 'Debit' if d.debit else 'Credit'
|
||||
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
|
||||
debit_or_credit)
|
||||
against_voucher = ['', against_voucher[1]]
|
||||
else:
|
||||
if d.reference_type == "Sales Invoice":
|
||||
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
||||
else:
|
||||
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}")
|
||||
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
|
||||
d.reference_type, d.reference_name))
|
||||
@@ -468,13 +469,22 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_against_account(self):
|
||||
accounts_debited, accounts_credited = [], []
|
||||
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)
|
||||
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
|
||||
for d in self.get('accounts'):
|
||||
if d.reference_type == 'Sales Invoice':
|
||||
field = 'customer'
|
||||
else:
|
||||
field = 'supplier'
|
||||
|
||||
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)))
|
||||
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
||||
else:
|
||||
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):
|
||||
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):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def update_accounts(source, target, source_parent):
|
||||
target.reference_type = "Journal Entry"
|
||||
target.reference_name = source_parent.name
|
||||
def post_process(source, target):
|
||||
target.reversal_of = source.name
|
||||
|
||||
doclist = get_mapped_doc("Journal Entry", source_name, {
|
||||
"Journal Entry": {
|
||||
@@ -1167,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
},
|
||||
"postprocess": update_accounts,
|
||||
}
|
||||
},
|
||||
}, target_doc)
|
||||
}, target_doc, post_process)
|
||||
|
||||
return doclist
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-07-25 14:57:33.187689",
|
||||
"modified": "2022-01-04 15:25:06.053187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
|
||||
@@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
|
||||
frappe.scrub(row.party_type): row.party,
|
||||
"is_pos": 0,
|
||||
"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()
|
||||
@@ -200,10 +201,13 @@ def start_import(invoices):
|
||||
names = []
|
||||
for idx, d in enumerate(invoices):
|
||||
try:
|
||||
invoice_number = None
|
||||
if d.invoice_number:
|
||||
invoice_number = d.invoice_number
|
||||
publish(idx, len(invoices), d.doctype)
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
frappe.db.commit()
|
||||
names.append(doc.name)
|
||||
|
||||
@@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice 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")
|
||||
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)
|
||||
return doc.make_invoices()
|
||||
|
||||
@@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
# teardown
|
||||
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):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
@@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"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,
|
||||
@@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"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",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"invoice_number",
|
||||
"party_type",
|
||||
"party",
|
||||
"temporary_opening_account",
|
||||
@@ -114,7 +116,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-17 19:25:06.053187",
|
||||
"modified": "2022-01-04 18:40:15.927675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
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.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"]:
|
||||
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
||||
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):
|
||||
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:
|
||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||
if term.discount_type == 'Percentage':
|
||||
|
||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_multi_mode_option,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
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"))
|
||||
|
||||
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):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||
@@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice):
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
elif d.batch_no:
|
||||
self.validate_pos_reserved_batch_qty(d)
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
return
|
||||
@@ -334,7 +354,6 @@ class POSInvoice(SalesInvoice):
|
||||
if not for_validate and not self.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.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")
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
pos_profile = None
|
||||
@@ -557,7 +623,8 @@ def create_pos_invoice(**args):
|
||||
"income_account": args.income_account or "Sales - _TC",
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _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:
|
||||
@@ -570,3 +637,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.payment_schedule = []
|
||||
|
||||
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,
|
||||
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
||||
"condition": args.condition or '',
|
||||
"priority": 1,
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.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):
|
||||
doc.db_set(applicable_for, args.get(applicable_for))
|
||||
|
||||
return doc
|
||||
|
||||
def setup_pricing_rule_data():
|
||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||
frappe.get_doc({
|
||||
|
||||
@@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController):
|
||||
# Checked both rounding_adjustment and 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
|
||||
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():
|
||||
# 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(
|
||||
self.get_gl_dict({
|
||||
"account": self.credit_to,
|
||||
@@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_expense_account,
|
||||
"credit": grand_total_in_company_currency,
|
||||
"credit_in_account_currency": grand_total_in_company_currency \
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_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_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):
|
||||
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',
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
||||
|
||||
@@ -651,7 +651,7 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -2038,7 +2038,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-21 20:19:38.667508",
|
||||
"modified": "2021-12-23 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -879,11 +879,11 @@ class SalesInvoice(SellingController):
|
||||
# Checked both rounding_adjustment and 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
|
||||
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():
|
||||
# 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(
|
||||
self.get_gl_dict({
|
||||
"account": self.debit_to,
|
||||
@@ -891,8 +891,8 @@ class SalesInvoice(SellingController):
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_income_account,
|
||||
"debit": grand_total_in_company_currency,
|
||||
"debit_in_account_currency": grand_total_in_company_currency \
|
||||
"debit": base_grand_total,
|
||||
"debit_in_account_currency": base_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_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.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
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.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
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")
|
||||
|
||||
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):
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
@@ -2358,6 +2318,41 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
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):
|
||||
si = frappe.copy_doc(test_records[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)
|
||||
|
||||
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():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "single_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Single Transaction Threshold"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"fieldname": "cumulative_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Cumulative Transaction Threshold"
|
||||
},
|
||||
@@ -59,7 +59,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-31 11:42:12.213977",
|
||||
"modified": "2022-01-13 12:04:42.904263",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Rate",
|
||||
@@ -68,5 +68,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"show_gl_balance",
|
||||
"label": __("Show GL Balance"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
],
|
||||
|
||||
onload: function(report) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
from six import iteritems
|
||||
|
||||
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,
|
||||
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):
|
||||
if party_dict.outstanding == 0:
|
||||
continue
|
||||
@@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
# but in summary report advance shown in separate column
|
||||
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)
|
||||
|
||||
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(_('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()
|
||||
|
||||
if self.party_type == "Customer":
|
||||
@@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# Add column for total due amount
|
||||
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.
|
||||
"""
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
if self.service_start_date != self.service_end_date:
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
|
||||
@@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "include_dimensions",
|
||||
"label": __("Consider Accounting Dimensions"),
|
||||
"fieldtype": "Check",
|
||||
"default": 0
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_opening_entries",
|
||||
|
||||
@@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
elif group_by_voucher_consolidated:
|
||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
if filters.get("include_dimensions"):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
consolidated_gle.setdefault(key, gle)
|
||||
@@ -595,14 +597,14 @@ def get_columns(filters):
|
||||
"fieldname": dim.fieldname,
|
||||
"width": 100
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
columns.append({
|
||||
"label": _("Cost Center"),
|
||||
"options": "Cost Center",
|
||||
"fieldname": "cost_center",
|
||||
"width": 100
|
||||
},
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
"label": _("Against Voucher Type"),
|
||||
"fieldname": "against_voucher_type",
|
||||
|
||||
@@ -583,7 +583,17 @@ class Asset(AccountsController):
|
||||
return purchase_document
|
||||
|
||||
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):
|
||||
cwip_account = None
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -113,7 +114,7 @@ class AccountsController(TransactionBase):
|
||||
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
|
||||
|
||||
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()
|
||||
|
||||
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))
|
||||
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))
|
||||
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):
|
||||
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():
|
||||
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||
today = getdate()
|
||||
|
||||
payment_schedule = frappe.qb.DocType("Payment Schedule")
|
||||
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
frappe.db.sql("""
|
||||
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
|
||||
WHERE invoice.docstatus = 1
|
||||
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
|
||||
AND invoice.outstanding_amount > 0
|
||||
AND (
|
||||
{or_condition}
|
||||
(
|
||||
(
|
||||
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}
|
||||
invoice = frappe.qb.DocType(doctype)
|
||||
|
||||
consider_base_amount = invoice.party_account_currency != invoice.currency
|
||||
payment_amount = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(consider_base_amount, payment_schedule.base_payment_amount)
|
||||
.else_(payment_schedule.payment_amount)
|
||||
)
|
||||
|
||||
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()
|
||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||
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()
|
||||
else:
|
||||
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()
|
||||
|
||||
if parent_doctype == 'Purchase Order':
|
||||
|
||||
@@ -385,7 +385,7 @@ class SellingController(StockController):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
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({
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
|
||||
@@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import (
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
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
|
||||
@@ -111,17 +111,6 @@ class StockController(AccountsController):
|
||||
|
||||
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
|
||||
if item_row.get('target_warehouse'):
|
||||
warehouse = item_row.get('target_warehouse')
|
||||
@@ -164,26 +153,6 @@ class StockController(AccountsController):
|
||||
|
||||
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):
|
||||
if self.doctype == "Stock Reconciliation":
|
||||
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"):
|
||||
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:
|
||||
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"]:
|
||||
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:
|
||||
total_amount_to_pay = flt(grand_total - self.doc.total_advance
|
||||
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
|
||||
else:
|
||||
total_amount_to_pay = flt(flt(grand_total *
|
||||
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
|
||||
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
|
||||
total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
|
||||
- flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
|
||||
|
||||
self.doc.round_floats_in(self.doc, ["paid_amount"])
|
||||
change_amount = 0
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers import queries
|
||||
|
||||
|
||||
@@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase):
|
||||
|
||||
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
||||
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):
|
||||
def test_reset_default_field_value(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Purchase Receipt",
|
||||
"set_warehouse": "Warehouse 1",
|
||||
})
|
||||
def test_reset_default_field_value(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Purchase Receipt",
|
||||
"set_warehouse": "Warehouse 1",
|
||||
})
|
||||
|
||||
# Same values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||
# Same values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||
|
||||
# Mixed values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, None)
|
||||
# Mixed values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
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
|
||||
# For license information, please see license.txt
|
||||
|
||||
import itertools
|
||||
import json
|
||||
|
||||
import frappe
|
||||
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
|
||||
context.body_class = "product-page"
|
||||
|
||||
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"],
|
||||
filters={"parent": self.item_code})
|
||||
filters={"parent": self.item_code}
|
||||
)
|
||||
|
||||
if self.slideshow:
|
||||
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_shopping_cart_data(context)
|
||||
|
||||
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
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):
|
||||
for variant in variants:
|
||||
variant.attributes = frappe.get_all(
|
||||
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||
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):
|
||||
context.metatags = frappe._dict({})
|
||||
|
||||
|
||||
@@ -197,7 +197,10 @@ class ProductQuery:
|
||||
website_item_groups = frappe.db.get_all(
|
||||
"Website Item",
|
||||
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
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
|
||||
val = frappe.cache().get_value('ordered_attribute_values_map')
|
||||
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')
|
||||
|
||||
ordered_attribute_values_map = frappe._dict({})
|
||||
@@ -57,25 +57,34 @@ class ItemVariantsCacheManager:
|
||||
def build_cache(self):
|
||||
parent_item_code = self.item_code
|
||||
|
||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
||||
attributes = [
|
||||
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',
|
||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
||||
# join with Website Item
|
||||
item_variants_data = frappe.get_all(
|
||||
'Item Variant Attribute',
|
||||
{'variant_of': parent_item_code},
|
||||
['parent', 'attribute', 'attribute_value'],
|
||||
order_by='name',
|
||||
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({})
|
||||
item_attribute_value_map = frappe._dict({})
|
||||
attribute_value_item_map = frappe._dict()
|
||||
item_attribute_value_map = frappe._dict()
|
||||
|
||||
# dont consider variants that are unpublished
|
||||
# (either have no Website Item or are unpublished in Website Item)
|
||||
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 frappe.db.exists("Website Item", {"item_code": r[0]})]
|
||||
# dont consider variants that are disabled
|
||||
# pull all other variants
|
||||
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||
|
||||
for row in item_variants_data:
|
||||
item_code, attribute, attribute_value = row
|
||||
|
||||
@@ -1,11 +1,96 @@
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
||||
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
import frappe
|
||||
|
||||
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"]
|
||||
|
||||
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
|
||||
if not age:
|
||||
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
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
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):
|
||||
@@ -171,7 +171,7 @@ def get_month_map():
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unmarked_days(employee, month):
|
||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
import calendar
|
||||
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]
|
||||
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 = []
|
||||
|
||||
for date in dates_of_month:
|
||||
|
||||
@@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = {
|
||||
onchange: function() {
|
||||
dialog.set_df_property("unmarked_days", "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("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
@@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = {
|
||||
onchange: function() {
|
||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("exclude_holidays", "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).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) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
@@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = {
|
||||
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"),
|
||||
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 => {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||
@@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = {
|
||||
args: {
|
||||
employee: employee,
|
||||
month: month,
|
||||
exclude_holidays: exclude_holidays
|
||||
}
|
||||
}).then(r => {
|
||||
var options = [];
|
||||
|
||||
@@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
def test_employee_onboarding_incomplete_task(self):
|
||||
if frappe.db.exists('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()
|
||||
|
||||
job_offer = create_job_offer(job_applicant=applicant.name)
|
||||
@@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
onboarding.submit()
|
||||
|
||||
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
|
||||
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
||||
@@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
self.assertEqual(employee.employee_name, 'Test Researcher')
|
||||
|
||||
def get_job_applicant():
|
||||
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
|
||||
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
|
||||
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
|
||||
return frappe.get_doc('Job Applicant', 'test@researcher.com')
|
||||
applicant = frappe.new_doc('Job Applicant')
|
||||
applicant.applicant_name = 'Test Researcher'
|
||||
applicant.email_id = 'test@researcher.com'
|
||||
|
||||
@@ -192,10 +192,11 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-29 23:06:10.904260",
|
||||
"modified": "2022-01-12 16:28:53.196881",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Job Applicant",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -210,10 +211,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "applicant_name",
|
||||
"search_fields": "applicant_name, email_id, job_title, phone_number",
|
||||
"sender_field": "email_id",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"subject_field": "notes",
|
||||
"title_field": "applicant_name"
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
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
|
||||
|
||||
def autoname(self):
|
||||
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
|
||||
if not keys:
|
||||
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
|
||||
self.name = " - ".join(keys)
|
||||
self.name = self.email_id
|
||||
|
||||
# applicant can apply more than once for a different job title or reapply
|
||||
if frappe.db.exists("Job Applicant", self.name):
|
||||
self.name = append_number_if_name_exists("Job Applicant", self.name)
|
||||
|
||||
def validate(self):
|
||||
if self.email_id:
|
||||
|
||||
@@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
|
||||
|
||||
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -1,294 +1,108 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "HR-LPR-.YYYY.-.#####",
|
||||
"beta": 0,
|
||||
"creation": "2018-04-13 15:20:52.864288",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_date",
|
||||
"to_date",
|
||||
"is_active",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"optional_holiday_list"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "from_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_standard_filter": 0,
|
||||
"label": "From Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "to_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_standard_filter": 0,
|
||||
"label": "To Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "is_active",
|
||||
"fieldtype": "Check",
|
||||
"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": "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
|
||||
"label": "Is Active"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "optional_holiday_list",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
"options": "Holiday List"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"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",
|
||||
"links": [],
|
||||
"modified": "2022-01-13 13:28:12.951025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Period",
|
||||
"name_case": "",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"search_fields": "from_date, to_date, company",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -113,10 +113,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-01 17:54:01.014509",
|
||||
"modified": "2022-01-13 13:37:11.218882",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Policy Assignment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -164,5 +165,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -129,6 +129,8 @@ class LeavePolicyAssignment(Document):
|
||||
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)
|
||||
new_leaves_allocated = monthly_earned_leave * months_passed
|
||||
else:
|
||||
new_leaves_allocated = 0
|
||||
|
||||
return new_leaves_allocated
|
||||
|
||||
|
||||
@@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
||||
if (cur_dialog.fields_dict.leave_period.value) {
|
||||
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"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, get_first_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
@@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
test_dependencies = ["Employee"]
|
||||
|
||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
|
||||
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
|
||||
|
||||
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.leave_type, "_Test Leave Type")
|
||||
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
|
||||
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
|
||||
self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_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_assignment, leave_policy_assignments[0])
|
||||
|
||||
@@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
# User are now allowed to grant leave
|
||||
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):
|
||||
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
||||
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
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', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(
|
||||
'Mark Attendance',
|
||||
() => frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'process_auto_attendance',
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frappe.msgprint(__("Attendance has been marked as per employee check-ins"));
|
||||
__('Mark Attendance'),
|
||||
() => {
|
||||
if (!frm.doc.enable_auto_attendance) {
|
||||
frm.scroll_to_field('enable_auto_attendance');
|
||||
frappe.throw(__('Please Enable Auto Attendance and complete the setup first.'));
|
||||
}
|
||||
})
|
||||
|
||||
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",
|
||||
"company",
|
||||
"posting_date",
|
||||
"is_term_loan",
|
||||
"rate_of_interest",
|
||||
"payroll_payable_account",
|
||||
"is_term_loan",
|
||||
"repay_from_salary",
|
||||
"payment_details_section",
|
||||
"due_date",
|
||||
"pending_principal_amount",
|
||||
@@ -243,15 +245,31 @@
|
||||
"label": "Total Penalty Paid",
|
||||
"options": "Company:company:default_currency",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:10:00.935364",
|
||||
"modified": "2022-01-06 01:51:06.707782",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -287,5 +305,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -321,74 +321,79 @@ class LoanRepayment(AccountsController):
|
||||
else:
|
||||
remarks = _("Repayment against Loan: ") + self.against_loan
|
||||
|
||||
if not loan_details.repay_from_salary:
|
||||
if self.total_penalty_paid:
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"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.repay_from_salary:
|
||||
payment_account = self.payroll_payable_account
|
||||
else:
|
||||
payment_account = loan_details.payment_account
|
||||
|
||||
if self.total_penalty_paid:
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party": loan_details.applicant,
|
||||
"against": loan_details.payment_account,
|
||||
"credit": self.amount_paid,
|
||||
"credit_in_account_currency": self.amount_paid,
|
||||
"debit": self.total_penalty_paid,
|
||||
"debit_in_account_currency": self.total_penalty_paid,
|
||||
"against_voucher_type": "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,
|
||||
"posting_date": getdate(self.posting_date)
|
||||
})
|
||||
)
|
||||
|
||||
if gle_map:
|
||||
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
|
||||
gle_map.append(
|
||||
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,
|
||||
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({
|
||||
"doctype": "Loan Repayment",
|
||||
@@ -401,7 +406,8 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
|
||||
"interest_payable": interest_payable,
|
||||
"payable_principal_amount": payable_principal_amount,
|
||||
"amount_paid": amount_paid,
|
||||
"loan_type": loan_type
|
||||
"loan_type": loan_type,
|
||||
"payroll_payable_account": payroll_payable_account
|
||||
}).insert()
|
||||
|
||||
return lr
|
||||
|
||||
@@ -531,16 +531,6 @@ class BOM(WebsiteGenerator):
|
||||
row.hour_rate = (hour_rate / flt(self.conversion_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:
|
||||
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
|
||||
|
||||
@@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase):
|
||||
wo_doc.delete()
|
||||
|
||||
def test_update_bom_operation_time(self):
|
||||
"""Update cost shouldn't update routing times."""
|
||||
operations = [
|
||||
{
|
||||
"operation": "Test Operation A",
|
||||
@@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase):
|
||||
routing_doc.save()
|
||||
bom_doc.update_cost()
|
||||
bom_doc.reload()
|
||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
|
||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
|
||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
|
||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
|
||||
|
||||
|
||||
def setup_operations(rows):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
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.production_plan.test_production_plan import make_bom
|
||||
@@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
OverProductionError,
|
||||
StockOverProductionError,
|
||||
close_work_order,
|
||||
make_job_card,
|
||||
make_stock_entry,
|
||||
stop_unstop,
|
||||
)
|
||||
@@ -801,6 +802,34 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
if row.is_scrap_item:
|
||||
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):
|
||||
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
|
||||
'Test RM Item 2 for Closed WO']
|
||||
@@ -841,7 +870,9 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
close_work_order(wo_order, "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.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', {
|
||||
'from_time': now(),
|
||||
'employee': employee,
|
||||
'time_in_mins': 60,
|
||||
'completed_qty': job_card_doc.for_quantity
|
||||
})
|
||||
|
||||
job_card_doc.submit()
|
||||
|
||||
|
||||
def get_scrap_item_details(bom_no):
|
||||
scrap_items = {}
|
||||
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);
|
||||
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."));
|
||||
} else {
|
||||
frm.trigger("show_progress_for_items");
|
||||
frm.trigger("show_progress_for_operations");
|
||||
}
|
||||
|
||||
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
|
||||
&& 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.repost_stock_ledger_entries_for_target_warehouse
|
||||
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.v12_0.fix_quotation_expired_status
|
||||
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.rename_discharge_ordered_date_in_ip_record
|
||||
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.reset_clearance_date_for_intracompany_payment_entries
|
||||
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.fetch_thumbnail_in_website_items
|
||||
erpnext.patches.v13_0.update_job_card_status
|
||||
erpnext.patches.v13_0.enable_uoms
|
||||
erpnext.patches.v12_0.update_production_plan_status
|
||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
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.update_tax_category_for_rcm
|
||||
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):
|
||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||
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):
|
||||
"""
|
||||
@@ -350,23 +352,24 @@ class PayrollEntry(Document):
|
||||
currencies = []
|
||||
multi_currency = 0
|
||||
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)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": self.payment_account,
|
||||
"bank_account": self.bank_account,
|
||||
"credit_in_account_currency": flt(amount, precision),
|
||||
"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)
|
||||
accounts.append({
|
||||
accounts.append(self.update_accounting_dimensions({
|
||||
"account": payroll_payable_account,
|
||||
"debit_in_account_currency": flt(amount, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"reference_type": self.doctype,
|
||||
"reference_name": self.name
|
||||
})
|
||||
}, accounting_dimensions))
|
||||
|
||||
if len(currencies) > 1:
|
||||
multi_currency = 1
|
||||
|
||||
@@ -936,8 +936,11 @@ class SalarySlip(TransactionBase):
|
||||
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
|
||||
future_recurring_additional_amount = 0
|
||||
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
|
||||
|
||||
# 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:
|
||||
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
|
||||
@@ -1036,7 +1039,8 @@ class SalarySlip(TransactionBase):
|
||||
data.update({"annual_taxable_earning": annual_taxable_earning})
|
||||
tax_amount = 0
|
||||
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
|
||||
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
|
||||
@@ -1142,15 +1146,17 @@ class SalarySlip(TransactionBase):
|
||||
})
|
||||
|
||||
def make_loan_repayment_entry(self):
|
||||
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
||||
for loan in self.loans:
|
||||
repayment_entry = create_repayment_entry(loan.loan, self.employee,
|
||||
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
|
||||
loan.principal_amount, loan.total_payment)
|
||||
if loan.total_payment:
|
||||
repayment_entry = create_repayment_entry(loan.loan, self.employee,
|
||||
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.submit()
|
||||
repayment_entry.save()
|
||||
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):
|
||||
for loan in self.loans:
|
||||
@@ -1384,3 +1390,11 @@ def get_salary_component_data(component):
|
||||
],
|
||||
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',
|
||||
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.repay_from_salary = 1
|
||||
loan.submit()
|
||||
|
||||
@@ -105,7 +105,7 @@ class Task(NestedSet):
|
||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||
|
||||
def update_depends_on(self):
|
||||
depends_on_tasks = self.depends_on_tasks or ""
|
||||
depends_on_tasks = ""
|
||||
for d in self.depends_on:
|
||||
if d.task and d.task not in depends_on_tasks:
|
||||
depends_on_tasks += d.task + ","
|
||||
|
||||
@@ -114,6 +114,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
|
||||
if ((!item.qty) && me.frm.doc.is_return) {
|
||||
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 {
|
||||
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"]);
|
||||
|
||||
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) {
|
||||
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
||||
- this.frm.doc.write_off_amount), precision("grand_total"));
|
||||
} else {
|
||||
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),
|
||||
precision("base_grand_total")
|
||||
);
|
||||
@@ -746,14 +749,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
},
|
||||
|
||||
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) {
|
||||
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
||||
- this.frm.doc.write_off_amount), precision("grand_total"));
|
||||
} else {
|
||||
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),
|
||||
precision("base_grand_total")
|
||||
);
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
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:
|
||||
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.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-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
|
||||
{ "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"
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ class Gstr1Report(object):
|
||||
port_code,
|
||||
shipping_bill_number,
|
||||
shipping_bill_date,
|
||||
reason_for_issuing_document
|
||||
reason_for_issuing_document,
|
||||
company_gstin
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
@@ -63,6 +64,8 @@ class Gstr1Report(object):
|
||||
self.get_b2c_data()
|
||||
elif self.filters.get("type_of_business") == "Advances":
|
||||
self.get_advance_data()
|
||||
elif self.filters.get("type_of_business") == "NIL Rated":
|
||||
self.get_nil_rated_invoices()
|
||||
elif self.invoices:
|
||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||
invoice_details = self.invoices.get(inv)
|
||||
@@ -92,6 +95,57 @@ class Gstr1Report(object):
|
||||
row= [key[0], key[1], value[0], value[1]]
|
||||
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):
|
||||
b2cs_output = {}
|
||||
|
||||
@@ -241,10 +295,11 @@ class Gstr1Report(object):
|
||||
def get_invoice_items(self):
|
||||
self.invoice_items = frappe._dict()
|
||||
self.item_tax_rate = frappe._dict()
|
||||
self.nil_exempt_non_gst = {}
|
||||
|
||||
items = frappe.db.sql("""
|
||||
select item_code, parent, taxable_value, base_net_amount, item_tax_rate
|
||||
from `tab%s Item`
|
||||
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
|
||||
is_non_gst from `tab%s Item`
|
||||
where parent in (%s)
|
||||
""" % (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.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):
|
||||
self.tax_details = frappe.db.sql("""
|
||||
select
|
||||
@@ -323,21 +388,24 @@ class Gstr1Report(object):
|
||||
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
||||
|
||||
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.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":
|
||||
self.invoice_columns = [
|
||||
@@ -706,6 +774,33 @@ class Gstr1Report(object):
|
||||
"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
|
||||
|
||||
@@ -769,6 +864,11 @@ def get_json(filters, report_name, data):
|
||||
out = get_advances_json(res, gstin)
|
||||
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 {
|
||||
'report_name': report_name,
|
||||
'report_type': filters['type_of_business'],
|
||||
@@ -981,6 +1081,36 @@ def get_cdnr_unreg_json(res, gstin):
|
||||
|
||||
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):
|
||||
if row.get('gst_category') == 'SEZ':
|
||||
if row.get('export_type') == 'WPAY':
|
||||
@@ -1065,3 +1195,9 @@ def download_json_file():
|
||||
frappe.response['filecontent'] = data['data']
|
||||
frappe.response['content_type'] = 'application/json'
|
||||
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",
|
||||
"label": _("Title"),
|
||||
"fieldtype": "Data",
|
||||
"width": 300
|
||||
"width": 300,
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"label": _("Amount (SAR)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "adjustment_amount",
|
||||
"label": _("Adjustment (SAR)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "vat_amount",
|
||||
"label": _("VAT Amount (SAR)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"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
|
||||
company = filters.get('company')
|
||||
company_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||
|
||||
if frappe.db.exists('KSA VAT Setting', company) is None:
|
||||
url = get_url_to_list('KSA VAT Setting')
|
||||
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)
|
||||
|
||||
# Sales Heading
|
||||
append_data(data, 'VAT on Sales', '', '', '')
|
||||
append_data(data, 'VAT on Sales', '', '', '', company_currency)
|
||||
|
||||
grand_total_taxable_amount = 0
|
||||
grand_total_taxable_adjustment_amount = 0
|
||||
@@ -67,7 +79,7 @@ def get_data(filters):
|
||||
|
||||
# Adding results to data
|
||||
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_adjustment_amount += total_taxable_adjustment_amount
|
||||
@@ -75,13 +87,13 @@ def get_data(filters):
|
||||
|
||||
# Sales Grand Total
|
||||
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
|
||||
append_data(data, '', '', '', '')
|
||||
append_data(data, '', '', '', '', company_currency)
|
||||
|
||||
# Purchase Heading
|
||||
append_data(data, 'VAT on Purchases', '', '', '')
|
||||
append_data(data, 'VAT on Purchases', '', '', '', company_currency)
|
||||
|
||||
grand_total_taxable_amount = 0
|
||||
grand_total_taxable_adjustment_amount = 0
|
||||
@@ -93,7 +105,7 @@ def get_data(filters):
|
||||
|
||||
# Adding results to data
|
||||
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_adjustment_amount += total_taxable_adjustment_amount
|
||||
@@ -101,7 +113,7 @@ def get_data(filters):
|
||||
|
||||
# Purchase Grand Total
|
||||
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
|
||||
|
||||
@@ -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."""
|
||||
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):
|
||||
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])
|
||||
})
|
||||
} 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]),
|
||||
indicator: 'orange'
|
||||
});
|
||||
|
||||
@@ -113,7 +113,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
`<div class="item-wrapper"
|
||||
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-rate="${escape(price_list_rate)}"
|
||||
data-rate="${escape(price_list_rate || 0)}"
|
||||
title="${item.item_name}">
|
||||
|
||||
${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.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
||||
soi.warehouse as warehouse,
|
||||
so.company, soi.name
|
||||
so.company, soi.name,
|
||||
soi.description as description
|
||||
FROM
|
||||
`tabSales Order` so,
|
||||
`tabSales Order Item` soi
|
||||
@@ -179,6 +180,12 @@ def get_columns(filters):
|
||||
"options": "Item",
|
||||
"width": 100
|
||||
})
|
||||
columns.append({
|
||||
"label":_("Description"),
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"width": 100
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
|
||||
@@ -79,14 +79,11 @@ frappe.ui.form.on("Company", {
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if(!frm.doc.__islocal) {
|
||||
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.is_new());
|
||||
|
||||
frm.toggle_display('address_html', !frm.doc.__islocal);
|
||||
if(!frm.doc.__islocal) {
|
||||
if (!frm.is_new()) {
|
||||
frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
|
||||
disbale_coa_fields(frm);
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
|
||||
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"}],
|
||||
["round_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", {}],
|
||||
["discount_allowed_account", {"root_type": "Expense"}],
|
||||
["discount_received_account", {"root_type": "Income"}],
|
||||
|
||||
@@ -49,6 +49,7 @@ class Company(NestedSet):
|
||||
self.validate_perpetual_inventory()
|
||||
self.validate_perpetual_inventory_for_non_stock_items()
|
||||
self.check_country_change()
|
||||
self.check_parent_changed()
|
||||
self.set_chart_of_accounts()
|
||||
self.validate_parent_company()
|
||||
|
||||
@@ -132,6 +133,10 @@ class Company(NestedSet):
|
||||
self.name in frappe.local.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()
|
||||
|
||||
def create_default_warehouses(self):
|
||||
@@ -193,7 +198,7 @@ class Company(NestedSet):
|
||||
def check_country_change(self):
|
||||
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'):
|
||||
frappe.flags.country_change = True
|
||||
|
||||
@@ -398,6 +403,13 @@ class Company(NestedSet):
|
||||
if not frappe.db.get_value('GL Entry', {'company': 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):
|
||||
company_abbr = frappe.get_cached_value('Company', company, "abbr")
|
||||
parts = name.split(" - ")
|
||||
|
||||
@@ -93,6 +93,61 @@ class TestCompany(unittest.TestCase):
|
||||
frappe.db.sql(""" delete from `tabMode of Payment Account`
|
||||
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):
|
||||
comm = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
|
||||
@@ -350,7 +350,8 @@ def add_uom_data():
|
||||
"doctype": "UOM",
|
||||
"uom_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()
|
||||
|
||||
# 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)
|
||||
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
|
||||
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}
|
||||
group by batch_id
|
||||
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"):
|
||||
args.doctype = "Batch"
|
||||
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):
|
||||
serial_nos = []
|
||||
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)
|
||||
|
||||
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):
|
||||
serial_nos = get_serial_nos(args.get('serial_no'))
|
||||
created_numbers = []
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
import frappe
|
||||
|
||||
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.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.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
@@ -21,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
|
||||
class TestSerialNo(ERPNextTestCase):
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_cannot_create_direct(self):
|
||||
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.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):
|
||||
"Test if Serial No input is sanitised before entering the DB."
|
||||
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")
|
||||
|
||||
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
|
||||
from frappe import _
|
||||
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 six import iteritems, itervalues, string_types
|
||||
|
||||
@@ -86,8 +87,11 @@ class StockEntry(StockController):
|
||||
self.validate_warehouse()
|
||||
self.validate_work_order()
|
||||
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_batch()
|
||||
self.validate_inspection()
|
||||
@@ -110,8 +114,12 @@ class StockEntry(StockController):
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount()
|
||||
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):
|
||||
self.update_stock_ledger()
|
||||
@@ -702,26 +710,25 @@ class StockEntry(StockController):
|
||||
validate_bom_no(item_code, d.bom_no)
|
||||
|
||||
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)]):
|
||||
return
|
||||
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
||||
return
|
||||
|
||||
finished_item = self.get_finished_item()
|
||||
finished_item = self.get_finished_item()
|
||||
|
||||
if not finished_item and self.purpose == "Manufacture":
|
||||
# In case of independent Manufacture entry, don't auto set
|
||||
# user must decide and set
|
||||
return
|
||||
if not finished_item and self.purpose == "Manufacture":
|
||||
# In case of independent Manufacture entry, don't auto set
|
||||
# user must decide and set
|
||||
return
|
||||
|
||||
for d in self.items:
|
||||
if d.t_warehouse and not d.s_warehouse:
|
||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||
d.is_finished_item = 1
|
||||
else:
|
||||
d.is_scrap_item = 1
|
||||
for d in self.items:
|
||||
if d.t_warehouse and not d.s_warehouse:
|
||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||
d.is_finished_item = 1
|
||||
else:
|
||||
d.is_finished_item = 0
|
||||
d.is_scrap_item = 0
|
||||
d.is_scrap_item = 1
|
||||
else:
|
||||
d.is_finished_item = 0
|
||||
d.is_scrap_item = 0
|
||||
|
||||
def get_finished_item(self):
|
||||
finished_item = None
|
||||
@@ -734,9 +741,9 @@ class StockEntry(StockController):
|
||||
|
||||
def validate_finished_goods(self):
|
||||
"""
|
||||
1. Check if FG exists
|
||||
2. Check if Multiple FG Items are present
|
||||
3. Check FG Item and Qty against WO if present
|
||||
1. Check if FG exists (mfg, repack)
|
||||
2. Check if Multiple FG Items are present (mfg)
|
||||
3. Check FG Item and Qty against WO if present (mfg)
|
||||
"""
|
||||
production_item, wo_qty, finished_items = None, 0, []
|
||||
|
||||
@@ -749,8 +756,9 @@ class StockEntry(StockController):
|
||||
for d in self.get('items'):
|
||||
if d.is_finished_item:
|
||||
if not self.work_order:
|
||||
# Independent MFG Entry/ Repack Entry, no WO to match against
|
||||
finished_items.append(d.item_code)
|
||||
continue # Independent Manufacture Entry, no WO to match against
|
||||
continue
|
||||
|
||||
if d.item_code != production_item:
|
||||
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)
|
||||
|
||||
if len(set(finished_items)) > 1:
|
||||
if not finished_items:
|
||||
frappe.throw(
|
||||
msg=_("Multiple items cannot be marked as finished item"),
|
||||
title=_("Note"),
|
||||
exc=FinishedGoodError
|
||||
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||
title=_("Missing Finished Good"), exc=FinishedGoodError
|
||||
)
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
if not finished_items:
|
||||
if len(set(finished_items)) > 1:
|
||||
frappe.throw(
|
||||
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||
title=_("Missing Finished Good"),
|
||||
exc=FinishedGoodError
|
||||
msg=_("Multiple items cannot be marked as finished item"),
|
||||
title=_("Note"), exc=FinishedGoodError
|
||||
)
|
||||
|
||||
allowance_percentage = flt(
|
||||
@@ -1276,22 +1282,29 @@ class StockEntry(StockController):
|
||||
if not self.pro_doc:
|
||||
self.set_work_order_details()
|
||||
|
||||
scrap_items = frappe.db.sql('''
|
||||
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:
|
||||
if not self.pro_doc.operations:
|
||||
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()
|
||||
for row in scrap_items:
|
||||
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||
@@ -1305,6 +1318,9 @@ class StockEntry(StockController):
|
||||
|
||||
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):
|
||||
used_scrap_items = defaultdict(float)
|
||||
data = frappe.get_all(
|
||||
|
||||
@@ -227,9 +227,47 @@ class TestStockEntry(ERPNextTestCase):
|
||||
|
||||
mtn.cancel()
|
||||
|
||||
def test_repack_no_change_in_valuation(self):
|
||||
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
|
||||
def test_repack_multiple_fg(self):
|
||||
"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 Home Desktop 100", target="_Test Warehouse - _TC",
|
||||
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].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):
|
||||
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
|
||||
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,
|
||||
posting_date, actual_qty
|
||||
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)
|
||||
|
||||
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]
|
||||
svd_list = frappe.get_list(
|
||||
'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)
|
||||
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',
|
||||
'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'):
|
||||
filters['item_code'] = report_filters.get('item_code')
|
||||
|
||||
@@ -76,6 +76,7 @@ def get_consumed_items(condition):
|
||||
on sle.voucher_no = se.name
|
||||
where
|
||||
actual_qty < 0
|
||||
and is_cancelled = 0
|
||||
and voucher_type not in ('Delivery Note', 'Sales Invoice')
|
||||
%s
|
||||
group by item_code""" % condition, as_dict=1)
|
||||
|
||||
@@ -105,6 +105,7 @@ def get_args_for_future_sle(row):
|
||||
|
||||
def validate_serial_no(sle):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
for sn in get_serial_nos(sle.serial_no):
|
||||
args = copy.deepcopy(sle)
|
||||
args.serial_no = sn
|
||||
@@ -415,6 +416,8 @@ class update_entries_after(object):
|
||||
return sorted(entries_to_fix, key=lambda k: k['timestamp'])
|
||||
|
||||
def process_sle(self, sle):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
# previous sle data for this warehouse
|
||||
self.wh_data = self.data[sle.warehouse]
|
||||
|
||||
@@ -429,7 +432,7 @@ class update_entries_after(object):
|
||||
if not self.args.get("sle_id"):
|
||||
self.get_dynamic_incoming_outgoing_rate(sle)
|
||||
|
||||
if sle.serial_no:
|
||||
if get_serial_nos(sle.serial_no):
|
||||
self.get_serialized_values(sle)
|
||||
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
|
||||
if sle.voucher_type == "Stock Reconciliation":
|
||||
@@ -441,8 +444,9 @@ class update_entries_after(object):
|
||||
# assert
|
||||
self.wh_data.valuation_rate = sle.valuation_rate
|
||||
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)
|
||||
if self.valuation_method != "Moving Average":
|
||||
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
|
||||
else:
|
||||
if self.valuation_method == "Moving Average":
|
||||
self.get_moving_average_values(sle)
|
||||
@@ -595,9 +599,9 @@ class update_entries_after(object):
|
||||
incoming_rate = self.wh_data.valuation_rate
|
||||
|
||||
stock_value_change = 0
|
||||
if incoming_rate:
|
||||
if actual_qty > 0:
|
||||
stock_value_change = actual_qty * incoming_rate
|
||||
elif actual_qty < 0:
|
||||
else:
|
||||
# In case of delivery/stock issue, get average purchase rate
|
||||
# of serial nos of current entry
|
||||
if not sle.is_cancelled:
|
||||
@@ -639,6 +643,7 @@ class update_entries_after(object):
|
||||
where
|
||||
company = %s
|
||||
and actual_qty > 0
|
||||
and is_cancelled = 0
|
||||
and (serial_no = %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
|
||||
AND warehouse = %s
|
||||
AND valuation_rate >= 0
|
||||
AND is_cancelled = 0
|
||||
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))
|
||||
|
||||
@@ -952,6 +958,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
|
||||
where
|
||||
item_code = %s
|
||||
AND valuation_rate > 0
|
||||
AND is_cancelled = 0
|
||||
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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
if not posting_date: posting_date = nowdate()
|
||||
if not posting_time: posting_time = nowtime()
|
||||
if posting_date is None: posting_date = nowdate()
|
||||
if posting_time is None: posting_time = nowtime()
|
||||
|
||||
args = {
|
||||
"item_code": item_code,
|
||||
|
||||
@@ -182,8 +182,6 @@ class TransactionBase(StatusUpdater):
|
||||
|
||||
if len(child_table_values) > 1:
|
||||
self.set(default_field, None)
|
||||
else:
|
||||
self.set(default_field, list(child_table_values)[0])
|
||||
|
||||
def delete_events(ref_type, ref_name):
|
||||
events = frappe.db.sql_list(""" SELECT
|
||||
|
||||
Reference in New Issue
Block a user