mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-15 19:19:17 +00:00
@@ -234,17 +234,19 @@ def get_checks_for_pl_and_bs_accounts():
|
|||||||
return dimensions
|
return dimensions
|
||||||
|
|
||||||
|
|
||||||
def get_dimension_with_children(doctype, dimension):
|
def get_dimension_with_children(doctype, dimensions):
|
||||||
|
|
||||||
if isinstance(dimension, list):
|
if isinstance(dimensions, str):
|
||||||
dimension = dimension[0]
|
dimensions = [dimensions]
|
||||||
|
|
||||||
all_dimensions = []
|
all_dimensions = []
|
||||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
|
||||||
children = frappe.get_all(
|
for dimension in dimensions:
|
||||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||||
)
|
children = frappe.get_all(
|
||||||
all_dimensions += [c.name for c in children]
|
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||||
|
)
|
||||||
|
all_dimensions += [c.name for c in children]
|
||||||
|
|
||||||
return all_dimensions
|
return all_dimensions
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,12 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
|
||||||
|
title=_("Invalid Invoice"),
|
||||||
|
)
|
||||||
|
|
||||||
if ref_doc.docstatus != 1:
|
if ref_doc.docstatus != 1:
|
||||||
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
|
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
|
||||||
|
|
||||||
|
|||||||
@@ -743,6 +743,21 @@ class TestPaymentEntry(unittest.TestCase):
|
|||||||
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
|
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||||
|
pi = make_purchase_invoice()
|
||||||
|
|
||||||
|
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = "2016-01-01"
|
||||||
|
|
||||||
|
# block invoice after creating payment entry
|
||||||
|
# since `get_payment_entry` will not attach blocked invoice to payment
|
||||||
|
pi.block_invoice()
|
||||||
|
with self.assertRaises(frappe.ValidationError) as err:
|
||||||
|
pe.save()
|
||||||
|
|
||||||
|
self.assertTrue("is on hold" in str(err.exception).lower())
|
||||||
|
|
||||||
|
|
||||||
def create_payment_entry(**args):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -3086,6 +3086,39 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.reload()
|
si.reload()
|
||||||
self.assertTrue(si.items[0].serial_no)
|
self.assertTrue(si.items[0].serial_no)
|
||||||
|
|
||||||
|
def test_sales_invoice_with_disabled_account(self):
|
||||||
|
try:
|
||||||
|
account = frappe.get_doc("Account", "VAT 5% - _TC")
|
||||||
|
account.disabled = 1
|
||||||
|
account.save()
|
||||||
|
|
||||||
|
si = create_sales_invoice(do_not_save=True)
|
||||||
|
si.posting_date = add_days(getdate(), 1)
|
||||||
|
si.taxes = []
|
||||||
|
|
||||||
|
si.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "VAT 5% - _TC",
|
||||||
|
"cost_center": "Main - _TC",
|
||||||
|
"description": "VAT @ 5.0",
|
||||||
|
"rate": 9,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.save()
|
||||||
|
|
||||||
|
with self.assertRaises(frappe.ValidationError) as err:
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
"Cannot create accounting entries against disabled accounts" in str(err.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
account.disabled = 0
|
||||||
|
account.save()
|
||||||
|
|
||||||
def test_gain_loss_with_advance_entry(self):
|
def test_gain_loss_with_advance_entry(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def make_gl_entries(
|
|||||||
if gl_map:
|
if gl_map:
|
||||||
if not cancel:
|
if not cancel:
|
||||||
validate_accounting_period(gl_map)
|
validate_accounting_period(gl_map)
|
||||||
|
validate_disabled_accounts(gl_map)
|
||||||
gl_map = process_gl_map(gl_map, merge_entries)
|
gl_map = process_gl_map(gl_map, merge_entries)
|
||||||
if gl_map and len(gl_map) > 1:
|
if gl_map and len(gl_map) > 1:
|
||||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||||
@@ -43,6 +44,26 @@ def make_gl_entries(
|
|||||||
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_disabled_accounts(gl_map):
|
||||||
|
accounts = [d.account for d in gl_map if d.account]
|
||||||
|
|
||||||
|
Account = frappe.qb.DocType("Account")
|
||||||
|
|
||||||
|
disabled_accounts = (
|
||||||
|
frappe.qb.from_(Account)
|
||||||
|
.where(Account.name.isin(accounts) & Account.disabled == 1)
|
||||||
|
.select(Account.name, Account.disabled)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if disabled_accounts:
|
||||||
|
account_list = "<br>"
|
||||||
|
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
|
||||||
|
frappe.throw(
|
||||||
|
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||||
|
title=_("Disabled Account Selected"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_accounting_period(gl_map):
|
def validate_accounting_period(gl_map):
|
||||||
accounting_periods = frappe.db.sql(
|
accounting_periods = frappe.db.sql(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
|||||||
)
|
)
|
||||||
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||||
else:
|
else:
|
||||||
additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
|
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||||
|
|
||||||
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
|
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
|
||||||
|
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ def get_conditions(filters):
|
|||||||
)
|
)
|
||||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||||
else:
|
else:
|
||||||
conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
|
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||||
|
|
||||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||||
|
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ def get_conditions(filters):
|
|||||||
else:
|
else:
|
||||||
conditions += (
|
conditions += (
|
||||||
common_condition
|
common_condition
|
||||||
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
|
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||||
)
|
)
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ def get_conditions(filters):
|
|||||||
else:
|
else:
|
||||||
conditions += (
|
conditions += (
|
||||||
common_condition
|
common_condition
|
||||||
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname)
|
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||||
)
|
)
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|||||||
@@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
|
|||||||
filters[dimension.fieldname] = get_dimension_with_children(
|
filters[dimension.fieldname] = get_dimension_with_children(
|
||||||
dimension.document_type, filters.get(dimension.fieldname)
|
dimension.document_type, filters.get(dimension.fieldname)
|
||||||
)
|
)
|
||||||
additional_conditions += "and {0} in %({0})s".format(dimension.fieldname)
|
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
|
||||||
else:
|
else:
|
||||||
additional_conditions += "and {0} in (%({0})s)".format(dimension.fieldname)
|
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
|
||||||
|
|
||||||
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})
|
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})
|
||||||
|
|
||||||
|
|||||||
@@ -353,15 +353,15 @@ class Asset(AccountsController):
|
|||||||
if self.allow_monthly_depreciation:
|
if self.allow_monthly_depreciation:
|
||||||
# month range is 1 to 12
|
# month range is 1 to 12
|
||||||
# In pro rata case, for first and last depreciation, month range would be different
|
# In pro rata case, for first and last depreciation, month range would be different
|
||||||
month_range = (
|
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
|
||||||
months
|
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
|
||||||
if (has_pro_rata and n == 0)
|
):
|
||||||
or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1)
|
month_range = months
|
||||||
else finance_book.frequency_of_depreciation
|
else:
|
||||||
)
|
month_range = finance_book.frequency_of_depreciation
|
||||||
|
|
||||||
for r in range(month_range):
|
for r in range(month_range):
|
||||||
if has_pro_rata and n == 0:
|
if has_pro_rata and n == 0 and not self.number_of_depreciations_booked:
|
||||||
# For first entry of monthly depr
|
# For first entry of monthly depr
|
||||||
if r == 0:
|
if r == 0:
|
||||||
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
|
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
|
||||||
|
|||||||
@@ -637,6 +637,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||||
|
|
||||||
|
stock_entry.set_missing_values()
|
||||||
return stock_entry.as_dict()
|
return stock_entry.as_dict()
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("No Items selected for transfer"))
|
frappe.throw(_("No Items selected for transfer"))
|
||||||
@@ -724,7 +726,7 @@ def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_deta
|
|||||||
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||||
|
|
||||||
ste_doc.set_stock_entry_type()
|
ste_doc.set_stock_entry_type()
|
||||||
ste_doc.calculate_rate_and_amount()
|
ste_doc.set_missing_values()
|
||||||
|
|
||||||
return ste_doc
|
return ste_doc
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
|
|
||||||
frm.add_custom_button(__('Supplier Quotation'),
|
frm.add_custom_button(__('Supplier Quotation'),
|
||||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
|
function(){ frm.trigger("make_supplier_quotation") }, __("Create"));
|
||||||
|
|
||||||
|
|
||||||
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
||||||
@@ -87,16 +87,24 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
make_suppplier_quotation: function(frm) {
|
make_supplier_quotation: function(frm) {
|
||||||
var doc = frm.doc;
|
var doc = frm.doc;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("Create Supplier Quotation"),
|
title: __("Create Supplier Quotation"),
|
||||||
fields: [
|
fields: [
|
||||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
{ "fieldtype": "Link",
|
||||||
|
"label": __("Supplier"),
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"options": doc.suppliers.map(d => d.supplier),
|
"options": 'Supplier',
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
filters: [
|
||||||
|
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
primary_action_label: __("Create"),
|
primary_action_label: __("Create"),
|
||||||
primary_action: (args) => {
|
primary_action: (args) => {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
"terms",
|
"terms",
|
||||||
"printing_settings",
|
"printing_settings",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"letter_head"
|
"letter_head",
|
||||||
|
"more_info",
|
||||||
|
"opportunity"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -193,6 +195,23 @@
|
|||||||
"options": "Letter Head",
|
"options": "Letter Head",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "more_info",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "More Information",
|
||||||
|
"oldfieldtype": "Section Break",
|
||||||
|
"options": "fa fa-file-text",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "opportunity",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Opportunity",
|
||||||
|
"options": "Opportunity",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -258,7 +277,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-11-24 17:47:49.909000",
|
"modified": "2022-04-06 17:47:49.909000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
self.validate_inter_company_reference()
|
self.validate_inter_company_reference()
|
||||||
|
|
||||||
|
self.disable_pricing_rule_on_internal_transfer()
|
||||||
self.set_incoming_rate()
|
self.set_incoming_rate()
|
||||||
|
|
||||||
if self.meta.get_field("currency"):
|
if self.meta.get_field("currency"):
|
||||||
@@ -383,6 +384,14 @@ class AccountsController(TransactionBase):
|
|||||||
msg += _("Please create purchase from internal sale or delivery document itself")
|
msg += _("Please create purchase from internal sale or delivery document itself")
|
||||||
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
||||||
|
|
||||||
|
def disable_pricing_rule_on_internal_transfer(self):
|
||||||
|
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||||
|
self.ignore_pricing_rule = 1
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Disabled pricing rules since this {} is an internal transfer").format(self.doctype),
|
||||||
|
alert=1,
|
||||||
|
)
|
||||||
|
|
||||||
def validate_due_date(self):
|
def validate_due_date(self):
|
||||||
if self.get("is_pos"):
|
if self.get("is_pos"):
|
||||||
return
|
return
|
||||||
@@ -1117,11 +1126,10 @@ class AccountsController(TransactionBase):
|
|||||||
{
|
{
|
||||||
"account": item.discount_account,
|
"account": item.discount_account,
|
||||||
"against": supplier_or_customer,
|
"against": supplier_or_customer,
|
||||||
dr_or_cr: flt(discount_amount, item.precision("discount_amount")),
|
dr_or_cr: flt(
|
||||||
dr_or_cr
|
|
||||||
+ "_in_account_currency": flt(
|
|
||||||
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
||||||
),
|
),
|
||||||
|
dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
|
||||||
"cost_center": item.cost_center,
|
"cost_center": item.cost_center,
|
||||||
"project": item.project,
|
"project": item.project,
|
||||||
},
|
},
|
||||||
@@ -1136,11 +1144,11 @@ class AccountsController(TransactionBase):
|
|||||||
{
|
{
|
||||||
"account": income_or_expense_account,
|
"account": income_or_expense_account,
|
||||||
"against": supplier_or_customer,
|
"against": supplier_or_customer,
|
||||||
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")),
|
rev_dr_cr: flt(
|
||||||
rev_dr_cr
|
|
||||||
+ "_in_account_currency": flt(
|
|
||||||
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
||||||
),
|
),
|
||||||
|
rev_dr_cr
|
||||||
|
+ "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
|
||||||
"cost_center": item.cost_center,
|
"cost_center": item.cost_center,
|
||||||
"project": item.project or self.project,
|
"project": item.project or self.project,
|
||||||
},
|
},
|
||||||
@@ -1736,6 +1744,8 @@ class AccountsController(TransactionBase):
|
|||||||
internal_party_field = "is_internal_customer"
|
internal_party_field = "is_internal_customer"
|
||||||
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
|
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
|
||||||
internal_party_field = "is_internal_supplier"
|
internal_party_field = "is_internal_supplier"
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
if self.get(internal_party_field) and (self.represents_company == self.company):
|
if self.get(internal_party_field) and (self.represents_company == self.company):
|
||||||
return True
|
return True
|
||||||
@@ -2449,11 +2459,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
|
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_quantity(child_item, d):
|
def validate_quantity(child_item, new_data):
|
||||||
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
|
if not flt(new_data.get("qty")):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
|
||||||
|
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||||
|
),
|
||||||
|
title=_("Invalid Qty"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
|
||||||
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
||||||
|
|
||||||
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
|
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
|
||||||
|
child_item.received_qty
|
||||||
|
):
|
||||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||||
|
|
||||||
data = json.loads(trans_items)
|
data = json.loads(trans_items)
|
||||||
|
|||||||
@@ -306,14 +306,15 @@ class BuyingController(StockController, Subcontracting):
|
|||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
if rate != d.rate:
|
if rate != d.rate:
|
||||||
d.rate = rate
|
d.rate = rate
|
||||||
d.discount_percentage = 0
|
|
||||||
d.discount_amount = 0
|
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_(
|
_(
|
||||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||||
).format(d.idx),
|
).format(d.idx),
|
||||||
alert=1,
|
alert=1,
|
||||||
)
|
)
|
||||||
|
d.discount_percentage = 0.0
|
||||||
|
d.discount_amount = 0.0
|
||||||
|
d.margin_rate_or_amount = 0.0
|
||||||
|
|
||||||
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
|
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
|
||||||
supplied_items_cost = 0.0
|
supplied_items_cost = 0.0
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{account_type_condition}
|
{account_type_condition}
|
||||||
AND is_group = 0
|
AND is_group = 0
|
||||||
AND company = %(company)s
|
AND company = %(company)s
|
||||||
|
AND disabled = %(disabled)s
|
||||||
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
||||||
AND `{searchfield}` LIKE %(txt)s
|
AND `{searchfield}` LIKE %(txt)s
|
||||||
{mcond}
|
{mcond}
|
||||||
@@ -175,6 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
dict(
|
dict(
|
||||||
account_types=filters.get("account_type"),
|
account_types=filters.get("account_type"),
|
||||||
company=filters.get("company"),
|
company=filters.get("company"),
|
||||||
|
disabled=filters.get("disabled", 0),
|
||||||
currency=company_currency,
|
currency=company_currency,
|
||||||
txt="%{}%".format(txt),
|
txt="%{}%".format(txt),
|
||||||
offset=start,
|
offset=start,
|
||||||
|
|||||||
@@ -446,15 +446,16 @@ class SellingController(StockController):
|
|||||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
|
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
|
||||||
if d.rate != rate:
|
if d.rate != rate:
|
||||||
d.rate = rate
|
d.rate = rate
|
||||||
|
frappe.msgprint(
|
||||||
|
_(
|
||||||
|
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||||
|
).format(d.idx),
|
||||||
|
alert=1,
|
||||||
|
)
|
||||||
|
|
||||||
d.discount_percentage = 0
|
d.discount_percentage = 0.0
|
||||||
d.discount_amount = 0
|
d.discount_amount = 0.0
|
||||||
frappe.msgprint(
|
d.margin_rate_or_amount = 0.0
|
||||||
_(
|
|
||||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
|
||||||
).format(d.idx),
|
|
||||||
alert=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.get("return_against"):
|
elif self.get("return_against"):
|
||||||
# Get incoming rate of return entry from reference document
|
# Get incoming rate of return entry from reference document
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.email.inbox import link_communication_to_document
|
from frappe.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, cstr, get_fullname
|
from frappe.utils import cint, get_fullname
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_account_currency
|
from erpnext.accounts.party import get_party_account_currency
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
@@ -193,20 +193,20 @@ class Opportunity(TransactionBase):
|
|||||||
|
|
||||||
if self.party_name and self.opportunity_from == "Customer":
|
if self.party_name and self.opportunity_from == "Customer":
|
||||||
if self.contact_person:
|
if self.contact_person:
|
||||||
opts.description = "Contact " + cstr(self.contact_person)
|
opts.description = f"Contact {self.contact_person}"
|
||||||
else:
|
else:
|
||||||
opts.description = "Contact customer " + cstr(self.party_name)
|
opts.description = f"Contact customer {self.party_name}"
|
||||||
elif self.party_name and self.opportunity_from == "Lead":
|
elif self.party_name and self.opportunity_from == "Lead":
|
||||||
if self.contact_display:
|
if self.contact_display:
|
||||||
opts.description = "Contact " + cstr(self.contact_display)
|
opts.description = f"Contact {self.contact_display}"
|
||||||
else:
|
else:
|
||||||
opts.description = "Contact lead " + cstr(self.party_name)
|
opts.description = f"Contact lead {self.party_name}"
|
||||||
|
|
||||||
opts.subject = opts.description
|
opts.subject = opts.description
|
||||||
opts.description += ". By : " + cstr(self.contact_by)
|
opts.description += f". By : {self.contact_by}"
|
||||||
|
|
||||||
if self.to_discuss:
|
if self.to_discuss:
|
||||||
opts.description += " To Discuss : " + cstr(self.to_discuss)
|
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
|
||||||
|
|
||||||
super(Opportunity, self).add_calendar_event(opts, force)
|
super(Opportunity, self).add_calendar_event(opts, force)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ def get_data():
|
|||||||
return {
|
return {
|
||||||
"fieldname": "opportunity",
|
"fieldname": "opportunity",
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"items": ["Quotation", "Supplier Quotation"]},
|
{"items": ["Quotation", "Request for Quotation", "Supplier Quotation"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import random_string, today
|
from frappe.utils import add_days, now_datetime, random_string, today
|
||||||
|
|
||||||
from erpnext.crm.doctype.lead.lead import make_customer
|
from erpnext.crm.doctype.lead.lead import make_customer
|
||||||
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
|
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
|
||||||
@@ -58,6 +58,22 @@ class TestOpportunity(unittest.TestCase):
|
|||||||
self.assertEqual(opp_doc.opportunity_from, "Customer")
|
self.assertEqual(opp_doc.opportunity_from, "Customer")
|
||||||
self.assertEqual(opp_doc.party_name, customer.name)
|
self.assertEqual(opp_doc.party_name, customer.name)
|
||||||
|
|
||||||
|
def test_render_template_for_to_discuss(self):
|
||||||
|
doc = make_opportunity(with_items=0, opportunity_from="Lead")
|
||||||
|
doc.contact_by = "test@example.com"
|
||||||
|
doc.contact_date = add_days(today(), days=2)
|
||||||
|
doc.to_discuss = "{{ doc.name }} test data"
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
event = frappe.get_all(
|
||||||
|
"Event Participants",
|
||||||
|
fields=["parent"],
|
||||||
|
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
event_description = frappe.db.get_value("Event", event[0].parent, "description")
|
||||||
|
self.assertTrue(doc.name in event_description)
|
||||||
|
|
||||||
|
|
||||||
def make_opportunity(**args):
|
def make_opportunity(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ doc_events = {
|
|||||||
"validate": [
|
"validate": [
|
||||||
"erpnext.regional.india.utils.validate_document_name",
|
"erpnext.regional.india.utils.validate_document_name",
|
||||||
"erpnext.regional.india.utils.update_taxable_values",
|
"erpnext.regional.india.utils.update_taxable_values",
|
||||||
|
"erpnext.regional.india.utils.validate_sez_and_export_invoices",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
|
"POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class Attendance(Document):
|
|||||||
self.validate_employee_status()
|
self.validate_employee_status()
|
||||||
self.check_leave_record()
|
self.check_leave_record()
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
self.unlink_attendance_from_checkins()
|
||||||
|
|
||||||
def validate_attendance_date(self):
|
def validate_attendance_date(self):
|
||||||
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
||||||
|
|
||||||
@@ -102,6 +105,35 @@ class Attendance(Document):
|
|||||||
if not emp:
|
if not emp:
|
||||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||||
|
|
||||||
|
def unlink_attendance_from_checkins(self):
|
||||||
|
from frappe.utils import get_link_to_form
|
||||||
|
|
||||||
|
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||||
|
linked_logs = (
|
||||||
|
frappe.qb.from_(EmployeeCheckin)
|
||||||
|
.select(EmployeeCheckin.name)
|
||||||
|
.where(EmployeeCheckin.attendance == self.name)
|
||||||
|
.for_update()
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if linked_logs:
|
||||||
|
(
|
||||||
|
frappe.qb.update(EmployeeCheckin)
|
||||||
|
.set("attendance", "")
|
||||||
|
.where(EmployeeCheckin.attendance == self.name)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
msg=_("Unlinked Attendance record from Employee Checkins: {}").format(
|
||||||
|
", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs)
|
||||||
|
),
|
||||||
|
title=_("Unlinked logs"),
|
||||||
|
indicator="blue",
|
||||||
|
is_minimizable=True,
|
||||||
|
wide=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_events(start, end, filters=None):
|
def get_events(start, end, filters=None):
|
||||||
|
|||||||
@@ -65,9 +65,10 @@ frappe.ui.form.on('Employee Advance', {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.docstatus === 1 &&
|
if (
|
||||||
(flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
|
frm.doc.docstatus === 1
|
||||||
|
&& (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount))
|
||||||
|
) {
|
||||||
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
|
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
|
||||||
frm.add_custom_button(__("Return"), function() {
|
frm.add_custom_button(__("Return"), function() {
|
||||||
frm.trigger('make_return_entry');
|
frm.trigger('make_return_entry');
|
||||||
|
|||||||
@@ -225,11 +225,15 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
|
|||||||
in_log = out_log = None
|
in_log = out_log = None
|
||||||
if not in_log:
|
if not in_log:
|
||||||
in_log = log if log.log_type == "IN" else None
|
in_log = log if log.log_type == "IN" else None
|
||||||
|
if in_log and not in_time:
|
||||||
|
in_time = in_log.time
|
||||||
elif not out_log:
|
elif not out_log:
|
||||||
out_log = log if log.log_type == "OUT" else None
|
out_log = log if log.log_type == "OUT" else None
|
||||||
|
|
||||||
if in_log and out_log:
|
if in_log and out_log:
|
||||||
out_time = out_log.time
|
out_time = out_log.time
|
||||||
total_hours += time_diff_in_hours(in_log.time, out_log.time)
|
total_hours += time_diff_in_hours(in_log.time, out_log.time)
|
||||||
|
|
||||||
return total_hours, in_time, out_time
|
return total_hours, in_time, out_time
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,17 @@ class TestEmployeeCheckin(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(attendance_count, 1)
|
self.assertEqual(attendance_count, 1)
|
||||||
|
|
||||||
|
def test_unlink_attendance_on_cancellation(self):
|
||||||
|
employee = make_employee("test_mark_attendance_and_link_log@example.com")
|
||||||
|
logs = make_n_checkins(employee, 3)
|
||||||
|
|
||||||
|
frappe.db.delete("Attendance", {"employee": employee})
|
||||||
|
attendance = mark_attendance_and_link_log(logs, "Present", nowdate(), 8.2)
|
||||||
|
attendance.cancel()
|
||||||
|
|
||||||
|
linked_logs = frappe.db.get_all("Employee Checkin", {"attendance": attendance.name})
|
||||||
|
self.assertEquals(len(linked_logs), 0)
|
||||||
|
|
||||||
def test_calculate_working_hours(self):
|
def test_calculate_working_hours(self):
|
||||||
check_in_out_type = [
|
check_in_out_type = [
|
||||||
"Alternating entries as IN and OUT during the same shift",
|
"Alternating entries as IN and OUT during the same shift",
|
||||||
@@ -103,6 +114,11 @@ class TestEmployeeCheckin(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
|
self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
|
||||||
|
|
||||||
|
working_hours = calculate_working_hours(
|
||||||
|
[logs_type_2[1], logs_type_2[-1]], check_in_out_type[1], working_hours_calc_type[1]
|
||||||
|
)
|
||||||
|
self.assertEqual(working_hours, (5.0, logs_type_2[1].time, logs_type_2[-1].time))
|
||||||
|
|
||||||
|
|
||||||
def make_n_checkins(employee, n, hours_to_reverse=1):
|
def make_n_checkins(employee, n, hours_to_reverse=1):
|
||||||
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))]
|
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))]
|
||||||
|
|||||||
@@ -4,21 +4,21 @@
|
|||||||
frappe.query_reports["Employee Leave Balance"] = {
|
frappe.query_reports["Employee Leave Balance"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"from_date",
|
"fieldname": "from_date",
|
||||||
"label": __("From Date"),
|
"label": __("From Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": frappe.defaults.get_default("year_start_date")
|
"default": frappe.defaults.get_default("year_start_date")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"to_date",
|
"fieldname": "to_date",
|
||||||
"label": __("To Date"),
|
"label": __("To Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": frappe.defaults.get_default("year_end_date")
|
"default": frappe.defaults.get_default("year_end_date")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"company",
|
"fieldname": "company",
|
||||||
"label": __("Company"),
|
"label": __("Company"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@@ -26,16 +26,29 @@ frappe.query_reports["Employee Leave Balance"] = {
|
|||||||
"default": frappe.defaults.get_user_default("Company")
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"department",
|
"fieldname": "department",
|
||||||
"label": __("Department"),
|
"label": __("Department"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Department",
|
"options": "Department",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"employee",
|
"fieldname": "employee",
|
||||||
"label": __("Employee"),
|
"label": __("Employee"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Employee",
|
"options": "Employee",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "employee_status",
|
||||||
|
"label": __("Employee Status"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [
|
||||||
|
"",
|
||||||
|
{ "value": "Active", "label": __("Active") },
|
||||||
|
{ "value": "Inactive", "label": __("Inactive") },
|
||||||
|
{ "value": "Suspended", "label": __("Suspended") },
|
||||||
|
{ "value": "Left", "label": __("Left") },
|
||||||
|
],
|
||||||
|
"default": "Active",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -168,9 +168,8 @@ def get_opening_balance(
|
|||||||
|
|
||||||
|
|
||||||
def get_conditions(filters: Filters) -> Dict:
|
def get_conditions(filters: Filters) -> Dict:
|
||||||
conditions = {
|
conditions = {}
|
||||||
"status": "Active",
|
|
||||||
}
|
|
||||||
if filters.get("employee"):
|
if filters.get("employee"):
|
||||||
conditions["name"] = filters.get("employee")
|
conditions["name"] = filters.get("employee")
|
||||||
|
|
||||||
@@ -180,6 +179,9 @@ def get_conditions(filters: Filters) -> Dict:
|
|||||||
if filters.get("department"):
|
if filters.get("department"):
|
||||||
conditions["department"] = filters.get("department")
|
conditions["department"] = filters.get("department")
|
||||||
|
|
||||||
|
if filters.get("employee_status"):
|
||||||
|
conditions["status"] = filters.get("employee_status")
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -207,3 +207,40 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
allocation1.new_leaves_allocated - leave_application.total_leave_days
|
allocation1.new_leaves_allocated - leave_application.total_leave_days
|
||||||
)
|
)
|
||||||
self.assertEqual(report[1][0].opening_balance, opening_balance)
|
self.assertEqual(report[1][0].opening_balance, opening_balance)
|
||||||
|
|
||||||
|
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
||||||
|
def test_employee_status_filter(self):
|
||||||
|
frappe.get_doc(test_records[0]).insert()
|
||||||
|
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
|
||||||
|
|
||||||
|
allocation = make_allocation_record(
|
||||||
|
employee=inactive_emp,
|
||||||
|
from_date=self.year_start,
|
||||||
|
to_date=self.year_end,
|
||||||
|
leaves=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# set employee as inactive
|
||||||
|
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"from_date": allocation.from_date,
|
||||||
|
"to_date": allocation.to_date,
|
||||||
|
"employee": inactive_emp,
|
||||||
|
"employee_status": "Active",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report = execute(filters)
|
||||||
|
self.assertEqual(len(report[1]), 0)
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"from_date": allocation.from_date,
|
||||||
|
"to_date": allocation.to_date,
|
||||||
|
"employee": inactive_emp,
|
||||||
|
"employee_status": "Inactive",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report = execute(filters)
|
||||||
|
self.assertEqual(len(report[1]), 1)
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ frappe.query_reports['Employee Leave Balance Summary'] = {
|
|||||||
label: __('Department'),
|
label: __('Department'),
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
options: 'Department',
|
options: 'Department',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "employee_status",
|
||||||
|
label: __("Employee Status"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: [
|
||||||
|
"",
|
||||||
|
{ "value": "Active", "label": __("Active") },
|
||||||
|
{ "value": "Inactive", "label": __("Inactive") },
|
||||||
|
{ "value": "Suspended", "label": __("Suspended") },
|
||||||
|
{ "value": "Left", "label": __("Left") },
|
||||||
|
],
|
||||||
|
default: "Active",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ def get_columns(leave_types):
|
|||||||
|
|
||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
conditions = {
|
conditions = {
|
||||||
"status": "Active",
|
|
||||||
"company": filters.company,
|
"company": filters.company,
|
||||||
}
|
}
|
||||||
|
if filters.get("employee_status"):
|
||||||
|
conditions.update({"status": filters.get("employee_status")})
|
||||||
if filters.get("department"):
|
if filters.get("department"):
|
||||||
conditions.update({"department": filters.get("department")})
|
conditions.update({"department": filters.get("department")})
|
||||||
if filters.get("employee"):
|
if filters.get("employee"):
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
|
|
||||||
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
|
self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
|
||||||
|
|
||||||
self.date = getdate()
|
self.date = getdate()
|
||||||
@@ -146,3 +145,37 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(report[1], expected_data)
|
self.assertEqual(report[1], expected_data)
|
||||||
|
|
||||||
|
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
||||||
|
def test_employee_status_filter(self):
|
||||||
|
frappe.get_doc(test_records[0]).insert()
|
||||||
|
|
||||||
|
inactive_emp = make_employee("test_emp_status@example.com", company="_Test Company")
|
||||||
|
allocation = make_allocation_record(
|
||||||
|
employee=inactive_emp, from_date=self.year_start, to_date=self.year_end
|
||||||
|
)
|
||||||
|
|
||||||
|
# set employee as inactive
|
||||||
|
frappe.db.set_value("Employee", inactive_emp, "status", "Inactive")
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"date": allocation.from_date,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"employee": inactive_emp,
|
||||||
|
"employee_status": "Active",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report = execute(filters)
|
||||||
|
self.assertEqual(len(report[1]), 0)
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"date": allocation.from_date,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"employee": inactive_emp,
|
||||||
|
"employee_status": "Inactive",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report = execute(filters)
|
||||||
|
self.assertEqual(len(report[1]), 1)
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ class LoanRepayment(AccountsController):
|
|||||||
regenerate_repayment_schedule(self.against_loan, cancel)
|
regenerate_repayment_schedule(self.against_loan, cancel)
|
||||||
|
|
||||||
def allocate_amounts(self, repayment_details):
|
def allocate_amounts(self, repayment_details):
|
||||||
|
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||||
self.set("repayment_details", [])
|
self.set("repayment_details", [])
|
||||||
self.principal_amount_paid = 0
|
self.principal_amount_paid = 0
|
||||||
self.total_penalty_paid = 0
|
self.total_penalty_paid = 0
|
||||||
@@ -279,9 +280,9 @@ class LoanRepayment(AccountsController):
|
|||||||
|
|
||||||
if interest_paid > 0:
|
if interest_paid > 0:
|
||||||
if self.penalty_amount and interest_paid > self.penalty_amount:
|
if self.penalty_amount and interest_paid > self.penalty_amount:
|
||||||
self.total_penalty_paid = self.penalty_amount
|
self.total_penalty_paid = flt(self.penalty_amount, precision)
|
||||||
elif self.penalty_amount:
|
elif self.penalty_amount:
|
||||||
self.total_penalty_paid = interest_paid
|
self.total_penalty_paid = flt(interest_paid, precision)
|
||||||
|
|
||||||
interest_paid -= self.total_penalty_paid
|
interest_paid -= self.total_penalty_paid
|
||||||
|
|
||||||
|
|||||||
@@ -764,8 +764,6 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
|
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
|
||||||
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
||||||
|
|
||||||
target.set_transfer_qty()
|
|
||||||
target.calculate_rate_and_amount()
|
|
||||||
target.set_missing_values()
|
target.set_missing_values()
|
||||||
target.set_stock_entry_type()
|
target.set_stock_entry_type()
|
||||||
|
|
||||||
|
|||||||
@@ -363,4 +363,6 @@ erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
|||||||
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||||
erpnext.patches.v13_0.set_available_for_use_date_if_missing
|
erpnext.patches.v13_0.set_available_for_use_date_if_missing
|
||||||
erpnext.patches.v13_0.education_deprecation_warning
|
erpnext.patches.v13_0.education_deprecation_warning
|
||||||
|
erpnext.patches.v13_0.requeue_recoverable_reposts
|
||||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||||
|
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||||
|
|||||||
21
erpnext/patches/v13_0/requeue_recoverable_reposts.py
Normal file
21
erpnext/patches/v13_0/requeue_recoverable_reposts.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
recoverable = ("QueryDeadlockError", "QueryTimeoutError", "JobTimeoutException")
|
||||||
|
|
||||||
|
failed_reposts = frappe.get_all(
|
||||||
|
"Repost Item Valuation",
|
||||||
|
fields=["name", "error_log"],
|
||||||
|
filters={
|
||||||
|
"status": "Failed",
|
||||||
|
"docstatus": 1,
|
||||||
|
"modified": (">", "2022-04-20"),
|
||||||
|
"error_log": ("is", "set"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for riv in failed_reposts:
|
||||||
|
for exc in recoverable:
|
||||||
|
if exc in riv.error_log:
|
||||||
|
frappe.db.set_value("Repost Item Valuation", riv.name, "status", "Queued")
|
||||||
|
break
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
dn = frappe.qb.DocType("Delivery Note")
|
||||||
|
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||||
|
|
||||||
|
dn_list = (
|
||||||
|
frappe.qb.from_(dn)
|
||||||
|
.inner_join(dn_item)
|
||||||
|
.on(dn.name == dn_item.parent)
|
||||||
|
.select(dn.name)
|
||||||
|
.where(dn.docstatus == 1)
|
||||||
|
.where(dn.is_return == 1)
|
||||||
|
.where(dn.per_billed < 100)
|
||||||
|
.where(dn_item.returned_qty > 0)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
|
||||||
|
dn_item.returned_qty, 0
|
||||||
|
).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
|
||||||
|
|
||||||
|
for d in dn_list:
|
||||||
|
dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
|
||||||
|
dn_doc.run_method("update_billing_status")
|
||||||
@@ -27,7 +27,8 @@ frappe.ui.form.on(cur_frm.doctype, {
|
|||||||
query: "erpnext.controllers.queries.tax_account_query",
|
query: "erpnext.controllers.queries.tax_account_query",
|
||||||
filters: {
|
filters: {
|
||||||
"account_type": account_type,
|
"account_type": account_type,
|
||||||
"company": doc.company
|
"company": doc.company,
|
||||||
|
"disabled": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
|||||||
else {
|
else {
|
||||||
return{
|
return{
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1 }
|
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1, 'has_variants': 0}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1035,12 +1035,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
currency: function() {
|
currency: function() {
|
||||||
/* manqala 19/09/2016: let the translation date be whichever of the transaction_date or posting_date is available */
|
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
||||||
var transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
|
||||||
/* end manqala */
|
let me = this;
|
||||||
var me = this;
|
|
||||||
this.set_dynamic_labels();
|
this.set_dynamic_labels();
|
||||||
var company_currency = this.get_company_currency();
|
let company_currency = this.get_company_currency();
|
||||||
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
|
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
|
||||||
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
|
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
|
||||||
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
|
||||||
@@ -1054,7 +1053,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.conversion_rate();
|
// company currency and doc currency is same
|
||||||
|
// this will prevent unnecessary conversion rate triggers
|
||||||
|
this.frm.set_value("conversion_rate", 1.0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1496,12 +1497,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
var me = this;
|
var me = this;
|
||||||
var args = this._get_args(item);
|
var args = this._get_args(item);
|
||||||
if (!(args.items && args.items.length)) {
|
if (!(args.items && args.items.length)) {
|
||||||
if(calculate_taxes_and_totals) me.calculate_taxes_and_totals();
|
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target doc created from a mapped doc
|
// Target doc created from a mapped doc
|
||||||
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
|
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
|
||||||
|
// Calculate totals even though pricing rule is not applied.
|
||||||
|
// `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total.
|
||||||
|
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ $.extend(erpnext.utils, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
add_indicator_for_multicompany: function(frm, info) {
|
add_indicator_for_multicompany: function(frm, info) {
|
||||||
frm.dashboard.stats_area.removeClass('hidden');
|
frm.dashboard.stats_area.show();
|
||||||
frm.dashboard.stats_area_row.addClass('flex');
|
frm.dashboard.stats_area_row.addClass('flex');
|
||||||
frm.dashboard.stats_area_row.css('flex-wrap', 'wrap');
|
frm.dashboard.stats_area_row.css('flex-wrap', 'wrap');
|
||||||
|
|
||||||
@@ -213,8 +213,10 @@ $.extend(erpnext.utils, {
|
|||||||
filters.splice(index, 0, {
|
filters.splice(index, 0, {
|
||||||
"fieldname": dimension["fieldname"],
|
"fieldname": dimension["fieldname"],
|
||||||
"label": __(dimension["label"]),
|
"label": __(dimension["label"]),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "MultiSelectList",
|
||||||
"options": dimension["document_type"]
|
get_data: function(txt) {
|
||||||
|
return frappe.db.get_link_options(dimension["document_type"], txt);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"autoname": "field:hsn_code",
|
"autoname": "field:hsn_code",
|
||||||
"creation": "2017-06-21 10:48:56.422086",
|
"creation": "2017-06-21 10:48:56.422086",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"hsn_code",
|
"hsn_code",
|
||||||
"description",
|
"description",
|
||||||
|
"gst_rates",
|
||||||
"taxes"
|
"taxes"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -16,22 +18,37 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "HSN Code",
|
"label": "HSN Code",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1,
|
||||||
"unique": 1
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Description"
|
"label": "Description",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "taxes",
|
"fieldname": "taxes",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Taxes",
|
"label": "Taxes",
|
||||||
"options": "Item Tax"
|
"options": "Item Tax",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "gst_rates",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "GST Rates",
|
||||||
|
"options": "HSN Tax Rate",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2019-11-01 11:18:59.556931",
|
"links": [],
|
||||||
|
"modified": "2022-05-11 13:42:27.286643",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Regional",
|
"module": "Regional",
|
||||||
"name": "GST HSN Code",
|
"name": "GST HSN Code",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
frappe.ui.form.on('GST Settings', {
|
frappe.ui.form.on('GST Settings', {
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.add_custom_button('Send GST Update Reminder', () => {
|
frm.add_custom_button(__('Send GST Update Reminder'), () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: 'erpnext.regional.doctype.gst_settings.gst_settings.send_reminder'
|
method: 'erpnext.regional.doctype.gst_settings.gst_settings.send_reminder'
|
||||||
@@ -11,6 +11,12 @@ frappe.ui.form.on('GST Settings', {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.add_custom_button(__('Sync HSN Codes'), () => {
|
||||||
|
frappe.call({
|
||||||
|
"method": "erpnext.regional.doctype.gst_settings.gst_settings.update_hsn_codes"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(frm.fields_dict.gst_summary.wrapper).empty().html(
|
$(frm.fields_dict.gst_summary.wrapper).empty().html(
|
||||||
`<table class="table table-bordered">
|
`<table class="table table-bordered">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.contacts.doctype.contact.contact import get_default_contact
|
from frappe.contacts.doctype.contact.contact import get_default_contact
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import date_diff, get_url, nowdate
|
from frappe.utils import date_diff, flt, get_url, nowdate
|
||||||
|
|
||||||
|
|
||||||
class EmailMissing(frappe.ValidationError):
|
class EmailMissing(frappe.ValidationError):
|
||||||
@@ -129,3 +130,31 @@ def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None)
|
|||||||
)
|
)
|
||||||
|
|
||||||
return email_id
|
return email_id
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_hsn_codes():
|
||||||
|
frappe.enqueue(enqueue_update)
|
||||||
|
frappe.msgprint(_("HSN/SAC Code sync started, this may take a few minutes..."))
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_update():
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f:
|
||||||
|
hsn_codes = json.loads(f.read())
|
||||||
|
|
||||||
|
for hsn_code in hsn_codes:
|
||||||
|
try:
|
||||||
|
hsn_code_doc = frappe.get_doc("GST HSN Code", hsn_code.get("hsn_code"))
|
||||||
|
hsn_code_doc.set("gst_rates", [])
|
||||||
|
for rate in hsn_code.get("gst_rates"):
|
||||||
|
hsn_code_doc.append(
|
||||||
|
"gst_rates",
|
||||||
|
{
|
||||||
|
"minimum_taxable_value": flt(hsn_code.get("minimum_taxable_value")),
|
||||||
|
"tax_rate": flt(rate.get("tax_rate")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
hsn_code_doc.save()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|||||||
144352
erpnext/regional/doctype/gst_settings/hsn_code_data.json
Normal file
144352
erpnext/regional/doctype/gst_settings/hsn_code_data.json
Normal file
File diff suppressed because it is too large
Load Diff
0
erpnext/regional/doctype/hsn_tax_rate/__init__.py
Normal file
0
erpnext/regional/doctype/hsn_tax_rate/__init__.py
Normal file
39
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.json
Normal file
39
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2022-05-11 13:32:42.534779",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"minimum_taxable_value",
|
||||||
|
"tax_rate"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "minimum_taxable_value",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Minimum Taxable Value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "tax_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Tax Rate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2022-05-15 15:37:56.152470",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Regional",
|
||||||
|
"name": "HSN Tax Rate",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
||||||
9
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.py
Normal file
9
erpnext/regional/doctype/hsn_tax_rate/hsn_tax_rate.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class HSNTaxRate(Document):
|
||||||
|
pass
|
||||||
@@ -841,6 +841,30 @@ def get_gst_accounts(
|
|||||||
return gst_accounts
|
return gst_accounts
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sez_and_export_invoices(doc, method):
|
||||||
|
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||||
|
|
||||||
|
if country != "India":
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
doc.get("gst_category") in ("SEZ", "Overseas")
|
||||||
|
and doc.get("export_type") == "Without Payment of Tax"
|
||||||
|
):
|
||||||
|
gst_accounts = get_gst_accounts(doc.company)
|
||||||
|
|
||||||
|
for tax in doc.get("taxes"):
|
||||||
|
for tax in doc.get("taxes"):
|
||||||
|
if (
|
||||||
|
tax.account_head
|
||||||
|
in gst_accounts.get("igst_account", [])
|
||||||
|
+ gst_accounts.get("sgst_account", [])
|
||||||
|
+ gst_accounts.get("cgst_account", [])
|
||||||
|
and tax.tax_amount_after_discount_amount
|
||||||
|
):
|
||||||
|
frappe.throw(_("GST cannot be applied on SEZ or Export invoices without payment of tax"))
|
||||||
|
|
||||||
|
|
||||||
def validate_reverse_charge_transaction(doc, method):
|
def validate_reverse_charge_transaction(doc, method):
|
||||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||||
|
|
||||||
@@ -888,6 +912,8 @@ def validate_reverse_charge_transaction(doc, method):
|
|||||||
|
|
||||||
frappe.throw(msg)
|
frappe.throw(msg)
|
||||||
|
|
||||||
|
doc.eligibility_for_itc = "ITC on Reverse Charge"
|
||||||
|
|
||||||
|
|
||||||
def update_itc_availed_fields(doc, method):
|
def update_itc_availed_fields(doc, method):
|
||||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||||
|
|||||||
@@ -227,7 +227,10 @@ class Gstr1Report(object):
|
|||||||
taxable_value += abs(net_amount)
|
taxable_value += abs(net_amount)
|
||||||
elif (
|
elif (
|
||||||
not tax_rate
|
not tax_rate
|
||||||
and self.filters.get("type_of_business") == "EXPORT"
|
and (
|
||||||
|
self.filters.get("type_of_business") == "EXPORT"
|
||||||
|
or invoice_details.get("gst_category") == "SEZ"
|
||||||
|
)
|
||||||
and invoice_details.get("export_type") == "Without Payment of Tax"
|
and invoice_details.get("export_type") == "Without Payment of Tax"
|
||||||
):
|
):
|
||||||
taxable_value += abs(net_amount)
|
taxable_value += abs(net_amount)
|
||||||
@@ -329,12 +332,14 @@ class Gstr1Report(object):
|
|||||||
def get_invoice_items(self):
|
def get_invoice_items(self):
|
||||||
self.invoice_items = frappe._dict()
|
self.invoice_items = frappe._dict()
|
||||||
self.item_tax_rate = frappe._dict()
|
self.item_tax_rate = frappe._dict()
|
||||||
|
self.item_hsn_map = frappe._dict()
|
||||||
self.nil_exempt_non_gst = {}
|
self.nil_exempt_non_gst = {}
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
items = frappe.db.sql(
|
items = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
|
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
|
||||||
is_non_gst from `tab%s Item`
|
gst_hsn_code, is_non_gst from `tab%s Item`
|
||||||
where parent in (%s)
|
where parent in (%s)
|
||||||
"""
|
"""
|
||||||
% (self.doctype, ", ".join(["%s"] * len(self.invoices))),
|
% (self.doctype, ", ".join(["%s"] * len(self.invoices))),
|
||||||
@@ -344,6 +349,7 @@ class Gstr1Report(object):
|
|||||||
|
|
||||||
for d in items:
|
for d in items:
|
||||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
|
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
|
||||||
|
self.item_hsn_map.setdefault(d.item_code, d.gst_hsn_code)
|
||||||
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
|
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
|
||||||
"base_net_amount", 0
|
"base_net_amount", 0
|
||||||
)
|
)
|
||||||
@@ -368,6 +374,8 @@ class Gstr1Report(object):
|
|||||||
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
|
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
|
||||||
|
|
||||||
def get_items_based_on_tax_rate(self):
|
def get_items_based_on_tax_rate(self):
|
||||||
|
hsn_wise_tax_rate = get_hsn_wise_tax_rates()
|
||||||
|
|
||||||
self.tax_details = frappe.db.sql(
|
self.tax_details = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
@@ -428,7 +436,7 @@ class Gstr1Report(object):
|
|||||||
alert=True,
|
alert=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build itemised tax for export invoices where tax table is blank
|
# Build itemised tax for export invoices where tax table is blank (Export and SEZ Invoices)
|
||||||
for invoice, items in iteritems(self.invoice_items):
|
for invoice, items in iteritems(self.invoice_items):
|
||||||
if (
|
if (
|
||||||
invoice not in self.items_based_on_tax_rate
|
invoice not in self.items_based_on_tax_rate
|
||||||
@@ -436,7 +444,17 @@ class Gstr1Report(object):
|
|||||||
and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax"
|
and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax"
|
||||||
and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ")
|
and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ")
|
||||||
):
|
):
|
||||||
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
self.items_based_on_tax_rate.setdefault(invoice, {})
|
||||||
|
for item_code in items.keys():
|
||||||
|
hsn_code = self.item_hsn_map.get(item_code)
|
||||||
|
tax_rate = 0
|
||||||
|
taxable_value = items.get(item_code)
|
||||||
|
for rates in hsn_wise_tax_rate.get(hsn_code):
|
||||||
|
if taxable_value > rates.get("minimum_taxable_value"):
|
||||||
|
tax_rate = rates.get("tax_rate")
|
||||||
|
|
||||||
|
self.items_based_on_tax_rate[invoice].setdefault(tax_rate, [])
|
||||||
|
self.items_based_on_tax_rate[invoice][tax_rate].append(item_code)
|
||||||
|
|
||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
self.other_columns = []
|
self.other_columns = []
|
||||||
@@ -729,7 +747,7 @@ def get_json(filters, report_name, data):
|
|||||||
|
|
||||||
elif filters["type_of_business"] == "EXPORT":
|
elif filters["type_of_business"] == "EXPORT":
|
||||||
for item in report_data[:-1]:
|
for item in report_data[:-1]:
|
||||||
res.setdefault(item["export_type"], []).append(item)
|
res.setdefault(item["export_type"], {}).setdefault(item["invoice_number"], []).append(item)
|
||||||
|
|
||||||
out = get_export_json(res)
|
out = get_export_json(res)
|
||||||
gst_json["exp"] = out
|
gst_json["exp"] = out
|
||||||
@@ -919,11 +937,21 @@ def get_export_json(res):
|
|||||||
for exp_type in res:
|
for exp_type in res:
|
||||||
exp_item, inv = {"exp_typ": exp_type, "inv": []}, []
|
exp_item, inv = {"exp_typ": exp_type, "inv": []}, []
|
||||||
|
|
||||||
for row in res[exp_type]:
|
for number, invoice in iteritems(res[exp_type]):
|
||||||
inv_item = get_basic_invoice_detail(row)
|
inv_item = get_basic_invoice_detail(invoice[0])
|
||||||
inv_item["itms"] = [
|
inv_item["itms"] = []
|
||||||
{"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0}
|
|
||||||
]
|
for item in invoice:
|
||||||
|
inv_item["itms"].append(
|
||||||
|
{
|
||||||
|
"txval": flt(item["taxable_value"], 2),
|
||||||
|
"rt": flt(item["rate"]),
|
||||||
|
"iamt": flt((item["taxable_value"] * flt(item["rate"])) / 100.0, 2)
|
||||||
|
if exp_type != "WOPAY"
|
||||||
|
else 0,
|
||||||
|
"csamt": (flt(item.get("cess_amount"), 2) or 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
inv.append(inv_item)
|
inv.append(inv_item)
|
||||||
|
|
||||||
@@ -1061,7 +1089,6 @@ def get_rate_and_tax_details(row, gstin):
|
|||||||
|
|
||||||
# calculate tax amount added
|
# calculate tax amount added
|
||||||
tax = flt((row["taxable_value"] * rate) / 100.0, 2)
|
tax = flt((row["taxable_value"] * rate) / 100.0, 2)
|
||||||
frappe.errprint([tax, tax / 2])
|
|
||||||
if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]:
|
if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]:
|
||||||
itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)})
|
itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)})
|
||||||
else:
|
else:
|
||||||
@@ -1137,3 +1164,26 @@ def get_company_gstins(company):
|
|||||||
address_list = [""] + [d.gstin for d in addresses]
|
address_list = [""] + [d.gstin for d in addresses]
|
||||||
|
|
||||||
return address_list
|
return address_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_hsn_wise_tax_rates():
|
||||||
|
hsn_wise_tax_rate = {}
|
||||||
|
gst_hsn_code = frappe.qb.DocType("GST HSN Code")
|
||||||
|
hsn_tax_rates = frappe.qb.DocType("HSN Tax Rate")
|
||||||
|
|
||||||
|
hsn_code_data = (
|
||||||
|
frappe.qb.from_(gst_hsn_code)
|
||||||
|
.inner_join(hsn_tax_rates)
|
||||||
|
.on(gst_hsn_code.name == hsn_tax_rates.parent)
|
||||||
|
.select(gst_hsn_code.hsn_code, hsn_tax_rates.tax_rate, hsn_tax_rates.minimum_taxable_value)
|
||||||
|
.orderby(hsn_tax_rates.minimum_taxable_value)
|
||||||
|
.run(as_dict=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in hsn_code_data:
|
||||||
|
hsn_wise_tax_rate.setdefault(d.hsn_code, [])
|
||||||
|
hsn_wise_tax_rate[d.hsn_code].append(
|
||||||
|
{"minimum_taxable_value": d.minimum_taxable_value, "tax_rate": d.tax_rate}
|
||||||
|
)
|
||||||
|
|
||||||
|
return hsn_wise_tax_rate
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ def get_merged_data(columns, data):
|
|||||||
result = []
|
result = []
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
key = row[0] + "-" + str(row[4])
|
key = row[0] + "-" + row[2] + "-" + str(row[4])
|
||||||
merged_hsn_dict.setdefault(key, {})
|
merged_hsn_dict.setdefault(key, {})
|
||||||
for i, d in enumerate(columns):
|
for i, d in enumerate(columns):
|
||||||
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
if d["fieldtype"] not in ("Int", "Float", "Currency"):
|
||||||
|
|||||||
@@ -369,7 +369,14 @@ def set_credit_limit(customer, company, credit_limit):
|
|||||||
customer.credit_limits[-1].db_insert()
|
customer.credit_limits[-1].db_insert()
|
||||||
|
|
||||||
|
|
||||||
def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
|
def create_internal_customer(
|
||||||
|
customer_name=None, represents_company=None, allowed_to_interact_with=None
|
||||||
|
):
|
||||||
|
if not customer_name:
|
||||||
|
customer_name = represents_company
|
||||||
|
if not allowed_to_interact_with:
|
||||||
|
allowed_to_interact_with = represents_company
|
||||||
|
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
customer = frappe.get_doc(
|
customer = frappe.get_doc(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,4 +238,5 @@ def get_chart_data(data):
|
|||||||
"datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}],
|
"datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}],
|
||||||
},
|
},
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters):
|
|||||||
},
|
},
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"lineOptions": {"regionFill": 1},
|
"lineOptions": {"regionFill": 1},
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,3 +416,8 @@ class Analytics(object):
|
|||||||
else:
|
else:
|
||||||
labels = [d.get("label") for d in self.columns[1 : length - 1]]
|
labels = [d.get("label") for d in self.columns[1 : length - 1]]
|
||||||
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
|
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
|
||||||
|
|
||||||
|
if self.filters["value_quantity"] == "Value":
|
||||||
|
self.chart["fieldtype"] = "Currency"
|
||||||
|
else:
|
||||||
|
self.chart["fieldtype"] = "Float"
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ def get_chart_data(data, conditions, filters):
|
|||||||
},
|
},
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"lineOptions": {"regionFill": 1},
|
"lineOptions": {"regionFill": 1},
|
||||||
|
"fieldtype": "Currency",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
|||||||
this.frm.set_query("item_code", "items", function() {
|
this.frm.set_query("item_code", "items", function() {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
|
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer, 'has_variants': 0}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,15 +570,12 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
customer=customer_name,
|
customer=customer_name,
|
||||||
cost_center="Main - TCP1",
|
cost_center="Main - TCP1",
|
||||||
expense_account="Cost of Goods Sold - TCP1",
|
expense_account="Cost of Goods Sold - TCP1",
|
||||||
do_not_submit=True,
|
|
||||||
qty=5,
|
qty=5,
|
||||||
rate=500,
|
rate=500,
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
target_warehouse=target_warehouse,
|
target_warehouse=target_warehouse,
|
||||||
)
|
)
|
||||||
|
|
||||||
dn.submit()
|
|
||||||
|
|
||||||
# qty after delivery
|
# qty after delivery
|
||||||
actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
|
actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
|
||||||
self.assertEqual(actual_qty_at_source, 475)
|
self.assertEqual(actual_qty_at_source, 475)
|
||||||
@@ -962,6 +959,111 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
|
|
||||||
automatically_fetch_payment_terms(enable=0)
|
automatically_fetch_payment_terms(enable=0)
|
||||||
|
|
||||||
|
def test_returned_qty_in_return_dn(self):
|
||||||
|
# SO ---> SI ---> DN
|
||||||
|
# |
|
||||||
|
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
|
||||||
|
# |
|
||||||
|
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||||
|
|
||||||
|
so = make_sales_order(qty=10)
|
||||||
|
si = make_sales_invoice(so.name)
|
||||||
|
si.insert()
|
||||||
|
si.submit()
|
||||||
|
dn = make_delivery_note(si.name)
|
||||||
|
dn.insert()
|
||||||
|
dn.submit()
|
||||||
|
self.assertEqual(dn.items[0].returned_qty, 0)
|
||||||
|
self.assertEqual(dn.per_billed, 100)
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||||
|
|
||||||
|
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
|
||||||
|
si1 = make_sales_invoice(dn1.name)
|
||||||
|
si1.insert()
|
||||||
|
si1.submit()
|
||||||
|
dn1.reload()
|
||||||
|
self.assertEqual(dn1.items[0].returned_qty, 0)
|
||||||
|
self.assertEqual(dn1.per_billed, 100)
|
||||||
|
|
||||||
|
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
|
||||||
|
si2 = make_sales_invoice(dn2.name)
|
||||||
|
si2.insert()
|
||||||
|
si2.submit()
|
||||||
|
dn2.reload()
|
||||||
|
self.assertEqual(dn2.items[0].returned_qty, 0)
|
||||||
|
self.assertEqual(dn2.per_billed, 100)
|
||||||
|
|
||||||
|
def test_internal_transfer_with_valuation_only(self):
|
||||||
|
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||||
|
|
||||||
|
item = make_item().name
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
target = "Stores - _TC"
|
||||||
|
company = "_Test Company"
|
||||||
|
customer = create_internal_customer(represents_company=company)
|
||||||
|
rate = 42
|
||||||
|
|
||||||
|
# Create item price and pricing rule
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"item_code": item,
|
||||||
|
"price_list": "Standard Selling",
|
||||||
|
"price_list_rate": 1000,
|
||||||
|
"doctype": "Item Price",
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": frappe.generate_hash(),
|
||||||
|
"apply_on": "Item Code",
|
||||||
|
"price_or_product_discount": "Price",
|
||||||
|
"selling": 1,
|
||||||
|
"company": company,
|
||||||
|
"margin_type": "Percentage",
|
||||||
|
"margin_rate_or_amount": 10,
|
||||||
|
"apply_discount_on": "Grand Total",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": item,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=item,
|
||||||
|
company=company,
|
||||||
|
customer=customer,
|
||||||
|
qty=5,
|
||||||
|
rate=500,
|
||||||
|
warehouse=warehouse,
|
||||||
|
target_warehouse=target,
|
||||||
|
ignore_pricing_rule=0,
|
||||||
|
do_not_save=True,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
|
||||||
|
dn.save()
|
||||||
|
self.assertEqual(dn.ignore_pricing_rule, 1)
|
||||||
|
|
||||||
|
# rate should reset to incoming rate
|
||||||
|
self.assertEqual(dn.items[0].rate, rate)
|
||||||
|
|
||||||
|
# rate should reset again if discounts are fiddled with
|
||||||
|
dn.items[0].margin_type = "Amount"
|
||||||
|
dn.items[0].margin_rate_or_amount = 50
|
||||||
|
dn.save()
|
||||||
|
|
||||||
|
self.assertEqual(dn.items[0].rate, rate)
|
||||||
|
|
||||||
|
|
||||||
def create_delivery_note(**args):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
|
|||||||
@@ -737,7 +737,9 @@
|
|||||||
"depends_on": "returned_qty",
|
"depends_on": "returned_qty",
|
||||||
"fieldname": "returned_qty",
|
"fieldname": "returned_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Returned Qty in Stock UOM"
|
"label": "Returned Qty in Stock UOM",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "incoming_rate",
|
"fieldname": "incoming_rate",
|
||||||
@@ -778,7 +780,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-31 18:36:24.671913",
|
"modified": "2022-05-02 12:09:39.610075",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
|
|||||||
@@ -586,8 +586,7 @@ $.extend(erpnext.item, {
|
|||||||
["parent","=", d.attribute]
|
["parent","=", d.attribute]
|
||||||
],
|
],
|
||||||
fields: ["attribute_value"],
|
fields: ["attribute_value"],
|
||||||
limit_start: 0,
|
limit_page_length: 0,
|
||||||
limit_page_length: 500,
|
|
||||||
parent: "Item Attribute",
|
parent: "Item Attribute",
|
||||||
order_by: "idx"
|
order_by: "idx"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
if source.material_request_type == "Customer Provided":
|
if source.material_request_type == "Customer Provided":
|
||||||
target.purpose = "Material Receipt"
|
target.purpose = "Material Receipt"
|
||||||
|
|
||||||
target.run_method("calculate_rate_and_amount")
|
target.set_missing_values()
|
||||||
target.set_stock_entry_type()
|
target.set_stock_entry_type()
|
||||||
target.set_job_card_data()
|
target.set_job_card_data()
|
||||||
|
|
||||||
|
|||||||
@@ -671,8 +671,7 @@ def create_stock_entry(pick_list):
|
|||||||
else:
|
else:
|
||||||
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
|
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
|
||||||
|
|
||||||
stock_entry.set_actual_qty()
|
stock_entry.set_missing_values()
|
||||||
stock_entry.calculate_rate_and_amount()
|
|
||||||
|
|
||||||
return stock_entry.as_dict()
|
return stock_entry.as_dict()
|
||||||
|
|
||||||
|
|||||||
@@ -993,6 +993,7 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.stock_entry_type = "Material Transfer"
|
target.stock_entry_type = "Material Transfer"
|
||||||
target.purpose = "Material Transfer"
|
target.purpose = "Material Transfer"
|
||||||
|
target.set_missing_values()
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
"Purchase Receipt",
|
"Purchase Receipt",
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
|
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
|
||||||
from frappe.utils.user import get_users_with_role
|
from frappe.utils.user import get_users_with_role
|
||||||
|
from rq.timeouts import JobTimeoutException
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
|
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
|
||||||
@@ -15,6 +17,8 @@ from erpnext.stock.stock_ledger import (
|
|||||||
repost_future_sle,
|
repost_future_sle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError)
|
||||||
|
|
||||||
|
|
||||||
class RepostItemValuation(Document):
|
class RepostItemValuation(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -132,7 +136,7 @@ def repost(doc):
|
|||||||
|
|
||||||
doc.set_status("Completed")
|
doc.set_status("Completed")
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
traceback = frappe.get_traceback()
|
traceback = frappe.get_traceback()
|
||||||
frappe.log_error(traceback)
|
frappe.log_error(traceback)
|
||||||
@@ -142,9 +146,9 @@ def repost(doc):
|
|||||||
message += "<br>" + "Traceback: <br>" + traceback
|
message += "<br>" + "Traceback: <br>" + traceback
|
||||||
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
||||||
|
|
||||||
notify_error_to_stock_managers(doc, message)
|
if not isinstance(e, RecoverableErrors):
|
||||||
doc.set_status("Failed")
|
notify_error_to_stock_managers(doc, message)
|
||||||
raise
|
doc.set_status("Failed")
|
||||||
finally:
|
finally:
|
||||||
if not frappe.flags.in_test:
|
if not frappe.flags.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|||||||
@@ -468,7 +468,9 @@ frappe.ui.form.on('Stock Entry', {
|
|||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
$.extend(child, r.message);
|
["actual_qty", "basic_rate"].forEach((field) => {
|
||||||
|
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
|
||||||
|
});
|
||||||
frm.events.calculate_basic_amount(frm, child);
|
frm.events.calculate_basic_amount(frm, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1069,8 +1071,8 @@ function attach_bom_items(bom_no) {
|
|||||||
|
|
||||||
function check_should_not_attach_bom_items(bom_no) {
|
function check_should_not_attach_bom_items(bom_no) {
|
||||||
return (
|
return (
|
||||||
bom_no === undefined ||
|
bom_no === undefined ||
|
||||||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
|
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -671,7 +671,8 @@ class StockEntry(StockController):
|
|||||||
raise_error_if_no_rate=raise_error_if_no_rate,
|
raise_error_if_no_rate=raise_error_if_no_rate,
|
||||||
)
|
)
|
||||||
|
|
||||||
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
|
# do not round off basic rate to avoid precision loss
|
||||||
|
d.basic_rate = flt(d.basic_rate)
|
||||||
if d.is_process_loss:
|
if d.is_process_loss:
|
||||||
d.basic_rate = flt(0.0)
|
d.basic_rate = flt(0.0)
|
||||||
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
||||||
@@ -718,7 +719,7 @@ class StockEntry(StockController):
|
|||||||
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
|
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
|
||||||
return flt(outgoing_items_cost / total_fg_qty)
|
return flt(outgoing_items_cost / total_fg_qty)
|
||||||
|
|
||||||
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
|
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
|
||||||
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
|
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
|
||||||
|
|
||||||
# Get raw materials cost from BOM if multiple material consumption entries
|
# Get raw materials cost from BOM if multiple material consumption entries
|
||||||
@@ -758,10 +759,8 @@ class StockEntry(StockController):
|
|||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.transfer_qty:
|
if d.transfer_qty:
|
||||||
d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount"))
|
d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount"))
|
||||||
d.valuation_rate = flt(
|
# Do not round off valuation rate to avoid precision loss
|
||||||
flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)),
|
d.valuation_rate = flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty))
|
||||||
d.precision("valuation_rate"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_total_incoming_outgoing_value(self):
|
def set_total_incoming_outgoing_value(self):
|
||||||
self.total_incoming_value = self.total_outgoing_value = 0.0
|
self.total_incoming_value = self.total_outgoing_value = 0.0
|
||||||
@@ -2196,6 +2195,12 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
||||||
|
|
||||||
|
def set_missing_values(self):
|
||||||
|
"Updates rate and availability of all the items of mapped doc."
|
||||||
|
self.set_transfer_qty()
|
||||||
|
self.set_actual_qty()
|
||||||
|
self.calculate_rate_and_amount()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def move_sample_to_retention_warehouse(company, items):
|
def move_sample_to_retention_warehouse(company, items):
|
||||||
@@ -2245,6 +2250,7 @@ def move_sample_to_retention_warehouse(company, items):
|
|||||||
def make_stock_in_entry(source_name, target_doc=None):
|
def make_stock_in_entry(source_name, target_doc=None):
|
||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.set_stock_entry_type()
|
target.set_stock_entry_type()
|
||||||
|
target.set_missing_values()
|
||||||
|
|
||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
target_doc.t_warehouse = ""
|
target_doc.t_warehouse = ""
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ def make_stock_entry(**args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
s.set_stock_entry_type()
|
s.set_stock_entry_type()
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
s.insert()
|
s.insert()
|
||||||
if not args.do_not_submit:
|
if not args.do_not_submit:
|
||||||
|
|||||||
@@ -1381,6 +1381,25 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, se.save)
|
self.assertRaises(frappe.ValidationError, se.save)
|
||||||
|
|
||||||
|
def test_mapped_stock_entry(self):
|
||||||
|
"Check if rate and stock details are populated in mapped SE given warehouse."
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_stock_entry
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
item_code = "_TestMappedItem"
|
||||||
|
create_item(item_code, is_stock_item=True)
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item_code, qty=2, rate=100, company="_Test Company", warehouse="Stores - _TC"
|
||||||
|
)
|
||||||
|
|
||||||
|
mapped_se = make_stock_entry(pr.name)
|
||||||
|
|
||||||
|
self.assertEqual(mapped_se.items[0].s_warehouse, "Stores - _TC")
|
||||||
|
self.assertEqual(mapped_se.items[0].actual_qty, 2)
|
||||||
|
self.assertEqual(mapped_se.items[0].basic_rate, 100)
|
||||||
|
self.assertEqual(mapped_se.items[0].basic_amount, 200)
|
||||||
|
|
||||||
|
|
||||||
def make_serialized_item(**args):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import frappe
|
|||||||
from frappe.core.page.permission_manager.permission_manager import reset
|
from frappe.core.page.permission_manager.permission_manager import reset
|
||||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, today
|
from frappe.utils import add_days, add_to_date, today
|
||||||
from frappe.utils.data import add_to_date
|
|
||||||
|
|
||||||
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
||||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
@@ -719,6 +718,41 @@ class TestStockLedgerEntry(FrappeTestCase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.fail("Double processing of qty for clashing timestamp.")
|
self.fail("Double processing of qty for clashing timestamp.")
|
||||||
|
|
||||||
|
@change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
|
||||||
|
def test_transfer_invariants(self):
|
||||||
|
"""Extact stock value should be transferred."""
|
||||||
|
|
||||||
|
item = make_item(
|
||||||
|
properties={
|
||||||
|
"valuation_method": "Moving Average",
|
||||||
|
"stock_uom": "Kg",
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
source_warehouse = "Stores - TCP1"
|
||||||
|
target_warehouse = "Finished Goods - TCP1"
|
||||||
|
|
||||||
|
make_purchase_receipt(
|
||||||
|
item=item,
|
||||||
|
warehouse=source_warehouse,
|
||||||
|
qty=20,
|
||||||
|
conversion_factor=1000,
|
||||||
|
uom="Tonne",
|
||||||
|
rate=156_526.0,
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
)
|
||||||
|
transfer = make_stock_entry(
|
||||||
|
item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = {"voucher_no": transfer.name, "voucher_type": transfer.doctype, "is_cancelled": 0}
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["*"],
|
||||||
|
filters=filters,
|
||||||
|
order_by="timestamp(posting_date, posting_time), creation",
|
||||||
|
)
|
||||||
|
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)
|
||||||
|
|
||||||
|
|
||||||
def create_repack_entry(**args):
|
def create_repack_entry(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
const DIFFERNCE_FIELD_NAMES = [
|
||||||
|
"fifo_qty_diff",
|
||||||
|
"fifo_value_diff",
|
||||||
|
];
|
||||||
|
|
||||||
|
frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item",
|
||||||
|
"options": "Item",
|
||||||
|
get_query: function() {
|
||||||
|
return {
|
||||||
|
filters: {is_stock_item: 1, has_serial_no: 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item Group",
|
||||||
|
"options": "Item Group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Warehouse",
|
||||||
|
"options": "Warehouse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Posting Date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Posting Date",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatter (value, row, column, data, default_formatter) {
|
||||||
|
value = default_formatter(value, row, column, data);
|
||||||
|
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
|
||||||
|
value = "<span style='color:red'>" + value + "</span>";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2022-05-11 04:09:13.460652",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"letter_head": "abc",
|
||||||
|
"modified": "2022-05-11 04:09:20.232177",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
|
"name": "FIFO Queue vs Qty After Transaction Comparison",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Stock Ledger Entry",
|
||||||
|
"report_name": "FIFO Queue vs Qty After Transaction Comparison",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Administrator"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
|
SLE_FIELDS = (
|
||||||
|
"name",
|
||||||
|
"item_code",
|
||||||
|
"warehouse",
|
||||||
|
"posting_date",
|
||||||
|
"posting_time",
|
||||||
|
"creation",
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"actual_qty",
|
||||||
|
"qty_after_transaction",
|
||||||
|
"stock_queue",
|
||||||
|
"batch_no",
|
||||||
|
"stock_value",
|
||||||
|
"valuation_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(filters):
|
||||||
|
if not any([filters.warehouse, filters.item_code, filters.item_group]):
|
||||||
|
frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
|
||||||
|
sles = get_stock_ledger_entries(filters)
|
||||||
|
return find_first_bad_queue(sles)
|
||||||
|
|
||||||
|
|
||||||
|
def get_stock_ledger_entries(filters):
|
||||||
|
|
||||||
|
sle_filters = {"is_cancelled": 0}
|
||||||
|
|
||||||
|
if filters.warehouse:
|
||||||
|
children = get_descendants_of("Warehouse", filters.warehouse)
|
||||||
|
sle_filters["warehouse"] = ("in", children + [filters.warehouse])
|
||||||
|
|
||||||
|
if filters.item_code:
|
||||||
|
sle_filters["item_code"] = filters.item_code
|
||||||
|
elif filters.get("item_group"):
|
||||||
|
item_group = filters.get("item_group")
|
||||||
|
children = get_descendants_of("Item Group", item_group)
|
||||||
|
item_group_filter = {"item_group": ("in", children + [item_group])}
|
||||||
|
sle_filters["item_code"] = (
|
||||||
|
"in",
|
||||||
|
frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters.from_date:
|
||||||
|
sle_filters["posting_date"] = (">=", filters.from_date)
|
||||||
|
if filters.to_date:
|
||||||
|
sle_filters["posting_date"] = ("<=", filters.to_date)
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=SLE_FIELDS,
|
||||||
|
filters=sle_filters,
|
||||||
|
order_by="timestamp(posting_date, posting_time), creation",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_first_bad_queue(sles):
|
||||||
|
item_warehouse_sles = {}
|
||||||
|
for sle in sles:
|
||||||
|
item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for _item_wh, sles in item_warehouse_sles.items():
|
||||||
|
for idx, sle in enumerate(sles):
|
||||||
|
queue = json.loads(sle.stock_queue or "[]")
|
||||||
|
|
||||||
|
sle.fifo_queue_qty = 0.0
|
||||||
|
sle.fifo_stock_value = 0.0
|
||||||
|
for qty, rate in queue:
|
||||||
|
sle.fifo_queue_qty += flt(qty)
|
||||||
|
sle.fifo_stock_value += flt(qty) * flt(rate)
|
||||||
|
|
||||||
|
sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
|
||||||
|
sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
|
||||||
|
|
||||||
|
if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
|
||||||
|
if idx:
|
||||||
|
data.append(sles[idx - 1])
|
||||||
|
data.append(sle)
|
||||||
|
data.append({})
|
||||||
|
break
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"fieldname": "name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Stock Ledger Entry"),
|
||||||
|
"options": "Stock Ledger Entry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Item Code"),
|
||||||
|
"options": "Item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Warehouse"),
|
||||||
|
"options": "Warehouse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Posting Date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_time",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Posting Time"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "creation",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("Creation"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Voucher Type"),
|
||||||
|
"options": "DocType",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": _("Voucher No"),
|
||||||
|
"options": "voucher_type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "batch_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Batch"),
|
||||||
|
"options": "Batch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "use_batchwise_valuation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": _("Batchwise Valuation"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "actual_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("Qty Change"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "qty_after_transaction",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(A) Qty After Transaction"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_queue",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": _("FIFO/LIFO Queue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_queue_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(C) Total qty in queue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_qty_diff",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("A - C"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_value",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(D) Balance Stock Value"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_stock_value",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(E) Balance Stock Value in Queue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fifo_value_diff",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("D - E"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "valuation_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": _("(H) Valuation Rate"),
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
@@ -148,18 +149,26 @@ def get_periodic_data(entry, filters):
|
|||||||
- Warehouse A : bal_qty/value
|
- Warehouse A : bal_qty/value
|
||||||
- Warehouse B : bal_qty/value
|
- Warehouse B : bal_qty/value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
expected_ranges = get_period_date_ranges(filters)
|
||||||
|
expected_periods = []
|
||||||
|
for _start_date, end_date in expected_ranges:
|
||||||
|
expected_periods.append(get_period(end_date, filters))
|
||||||
|
|
||||||
periodic_data = {}
|
periodic_data = {}
|
||||||
for d in entry:
|
for d in entry:
|
||||||
period = get_period(d.posting_date, filters)
|
period = get_period(d.posting_date, filters)
|
||||||
bal_qty = 0
|
bal_qty = 0
|
||||||
|
|
||||||
|
fill_intermediate_periods(periodic_data, d.item_code, period, expected_periods)
|
||||||
|
|
||||||
# if period against item does not exist yet, instantiate it
|
# if period against item does not exist yet, instantiate it
|
||||||
# insert existing balance dict against period, and add/subtract to it
|
# insert existing balance dict against period, and add/subtract to it
|
||||||
if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period):
|
if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period):
|
||||||
previous_balance = periodic_data[d.item_code]["balance"].copy()
|
previous_balance = periodic_data[d.item_code]["balance"].copy()
|
||||||
periodic_data[d.item_code][period] = previous_balance
|
periodic_data[d.item_code][period] = previous_balance
|
||||||
|
|
||||||
if d.voucher_type == "Stock Reconciliation":
|
if d.voucher_type == "Stock Reconciliation" and not d.batch_no:
|
||||||
if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get(
|
if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get(
|
||||||
d.warehouse
|
d.warehouse
|
||||||
):
|
):
|
||||||
@@ -186,6 +195,36 @@ def get_periodic_data(entry, filters):
|
|||||||
return periodic_data
|
return periodic_data
|
||||||
|
|
||||||
|
|
||||||
|
def fill_intermediate_periods(
|
||||||
|
periodic_data, item_code: str, current_period: str, all_periods: List[str]
|
||||||
|
) -> None:
|
||||||
|
"""There might be intermediate periods where no stock ledger entry exists, copy previous previous data.
|
||||||
|
|
||||||
|
Previous data is ONLY copied if period falls in report range and before period being processed currently.
|
||||||
|
|
||||||
|
args:
|
||||||
|
current_period: process till this period (exclusive)
|
||||||
|
all_periods: all periods expected in report via filters
|
||||||
|
periodic_data: report's periodic data
|
||||||
|
item_code: item_code being processed
|
||||||
|
"""
|
||||||
|
|
||||||
|
previous_period_data = None
|
||||||
|
for period in all_periods:
|
||||||
|
if period == current_period:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
periodic_data.get(item_code)
|
||||||
|
and not periodic_data.get(item_code).get(period)
|
||||||
|
and previous_period_data
|
||||||
|
):
|
||||||
|
# This period should exist since it's in report range, assign previous period data
|
||||||
|
periodic_data[item_code][period] = previous_period_data.copy()
|
||||||
|
|
||||||
|
previous_period_data = periodic_data.get(item_code, {}).get(period)
|
||||||
|
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
data = []
|
data = []
|
||||||
items = get_items(filters)
|
items = get_items(filters)
|
||||||
@@ -194,6 +233,8 @@ def get_data(filters):
|
|||||||
periodic_data = get_periodic_data(sle, filters)
|
periodic_data = get_periodic_data(sle, filters)
|
||||||
ranges = get_period_date_ranges(filters)
|
ranges = get_period_date_ranges(filters)
|
||||||
|
|
||||||
|
today = getdate()
|
||||||
|
|
||||||
for dummy, item_data in item_details.items():
|
for dummy, item_data in item_details.items():
|
||||||
row = {
|
row = {
|
||||||
"name": item_data.name,
|
"name": item_data.name,
|
||||||
@@ -202,14 +243,15 @@ def get_data(filters):
|
|||||||
"uom": item_data.stock_uom,
|
"uom": item_data.stock_uom,
|
||||||
"brand": item_data.brand,
|
"brand": item_data.brand,
|
||||||
}
|
}
|
||||||
total = 0
|
previous_period_value = 0.0
|
||||||
for dummy, end_date in ranges:
|
for start_date, end_date in ranges:
|
||||||
period = get_period(end_date, filters)
|
period = get_period(end_date, filters)
|
||||||
period_data = periodic_data.get(item_data.name, {}).get(period)
|
period_data = periodic_data.get(item_data.name, {}).get(period)
|
||||||
amount = sum(period_data.values()) if period_data else 0
|
if period_data:
|
||||||
row[scrub(period)] = amount
|
row[scrub(period)] = previous_period_value = sum(period_data.values())
|
||||||
total += amount
|
else:
|
||||||
row["total"] = total
|
row[scrub(period)] = previous_period_value if today >= start_date else None
|
||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -1,13 +1,59 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe import _dict
|
from frappe import _dict
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils.data import add_to_date, get_datetime, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
from erpnext.stock.report.stock_analytics.stock_analytics import execute, get_period_date_ranges
|
||||||
|
|
||||||
|
|
||||||
|
def stock_analytics(filters):
|
||||||
|
col, data, *_ = execute(filters)
|
||||||
|
return col, data
|
||||||
|
|
||||||
|
|
||||||
class TestStockAnalyticsReport(FrappeTestCase):
|
class TestStockAnalyticsReport(FrappeTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.item = make_item().name
|
||||||
|
self.warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
def assert_single_item_report(self, movement, expected_buckets):
|
||||||
|
self.generate_stock(movement)
|
||||||
|
filters = _dict(
|
||||||
|
range="Monthly",
|
||||||
|
from_date=movement[0][1].replace(day=1),
|
||||||
|
to_date=movement[-1][1].replace(day=28),
|
||||||
|
value_quantity="Quantity",
|
||||||
|
company="_Test Company",
|
||||||
|
item_code=self.item,
|
||||||
|
)
|
||||||
|
|
||||||
|
cols, data = stock_analytics(filters)
|
||||||
|
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
row = frappe._dict(data[0])
|
||||||
|
self.assertEqual(row.name, self.item)
|
||||||
|
self.compare_analytics_row(row, cols, expected_buckets)
|
||||||
|
|
||||||
|
def generate_stock(self, movement):
|
||||||
|
for qty, posting_date in movement:
|
||||||
|
args = {"item": self.item, "qty": abs(qty), "posting_date": posting_date}
|
||||||
|
args["to_warehouse" if qty > 0 else "from_warehouse"] = self.warehouse
|
||||||
|
make_stock_entry(**args)
|
||||||
|
|
||||||
|
def compare_analytics_row(self, report_row, columns, expected_buckets):
|
||||||
|
# last (N) cols will be monthly data
|
||||||
|
no_of_buckets = len(expected_buckets)
|
||||||
|
month_cols = [col["fieldname"] for col in columns[-no_of_buckets:]]
|
||||||
|
|
||||||
|
actual_buckets = [report_row.get(col) for col in month_cols]
|
||||||
|
|
||||||
|
self.assertEqual(actual_buckets, expected_buckets)
|
||||||
|
|
||||||
def test_get_period_date_ranges(self):
|
def test_get_period_date_ranges(self):
|
||||||
|
|
||||||
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
|
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
|
||||||
@@ -33,3 +79,38 @@ class TestStockAnalyticsReport(FrappeTestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(ranges, expected_ranges)
|
self.assertEqual(ranges, expected_ranges)
|
||||||
|
|
||||||
|
def test_basic_report_functionality(self):
|
||||||
|
"""Stock analytics report generates balance "as of" periods based on
|
||||||
|
user defined ranges. Check that this behaviour is correct."""
|
||||||
|
|
||||||
|
# create stock movement in 3 months at 15th of month
|
||||||
|
today = getdate()
|
||||||
|
movement = [
|
||||||
|
(10, add_to_date(today, months=0).replace(day=15)),
|
||||||
|
(-5, add_to_date(today, months=1).replace(day=15)),
|
||||||
|
(10, add_to_date(today, months=2).replace(day=15)),
|
||||||
|
]
|
||||||
|
self.assert_single_item_report(movement, [10, 5, 15])
|
||||||
|
|
||||||
|
def test_empty_month_in_between(self):
|
||||||
|
today = getdate()
|
||||||
|
movement = [
|
||||||
|
(100, add_to_date(today, months=0).replace(day=15)),
|
||||||
|
(-50, add_to_date(today, months=1).replace(day=15)),
|
||||||
|
# Skip a month
|
||||||
|
(20, add_to_date(today, months=3).replace(day=15)),
|
||||||
|
]
|
||||||
|
self.assert_single_item_report(movement, [100, 50, 50, 70])
|
||||||
|
|
||||||
|
def test_multi_month_missings(self):
|
||||||
|
today = getdate()
|
||||||
|
movement = [
|
||||||
|
(100, add_to_date(today, months=0).replace(day=15)),
|
||||||
|
(-50, add_to_date(today, months=1).replace(day=15)),
|
||||||
|
# Skip a month
|
||||||
|
(20, add_to_date(today, months=3).replace(day=15)),
|
||||||
|
# Skip another month
|
||||||
|
(-10, add_to_date(today, months=5).replace(day=15)),
|
||||||
|
]
|
||||||
|
self.assert_single_item_report(movement, [100, 50, 50, 70, 70, 60])
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
|||||||
("Batch Item Expiry Status", {}),
|
("Batch Item Expiry Status", {}),
|
||||||
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
|
||||||
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
|
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
|
||||||
|
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
|
||||||
|
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
|
||||||
]
|
]
|
||||||
|
|
||||||
OPTIONAL_FILTERS = {
|
OPTIONAL_FILTERS = {
|
||||||
|
|||||||
@@ -41,3 +41,8 @@ class TestInit(unittest.TestCase):
|
|||||||
enc_name == expected_names[i],
|
enc_name == expected_names[i],
|
||||||
"{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]),
|
"{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_translation_files(self):
|
||||||
|
from frappe.tests.test_translate import verify_translation_files
|
||||||
|
|
||||||
|
verify_translation_files("erpnext")
|
||||||
|
|||||||
@@ -5055,7 +5055,7 @@ Accepted Qty,Akzeptierte Menge,
|
|||||||
Rejected Qty,Abgelehnt Menge,
|
Rejected Qty,Abgelehnt Menge,
|
||||||
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
|
UOM Conversion Factor,Maßeinheit-Umrechnungsfaktor,
|
||||||
Discount on Price List Rate (%),Rabatt auf die Preisliste (%),
|
Discount on Price List Rate (%),Rabatt auf die Preisliste (%),
|
||||||
Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
|
Price List Rate (Company Currency),Preisliste (Unternehmenswährung),
|
||||||
Rate ,Preis,
|
Rate ,Preis,
|
||||||
Rate (Company Currency),Preis (Unternehmenswährung),
|
Rate (Company Currency),Preis (Unternehmenswährung),
|
||||||
Amount (Company Currency),Betrag (Unternehmenswährung),
|
Amount (Company Currency),Betrag (Unternehmenswährung),
|
||||||
@@ -6509,20 +6509,20 @@ Driver licence class,Führerscheinklasse,
|
|||||||
HR-EMP-,HR-EMP-,
|
HR-EMP-,HR-EMP-,
|
||||||
Employment Type,Art der Beschäftigung,
|
Employment Type,Art der Beschäftigung,
|
||||||
Emergency Contact,Notfallkontakt,
|
Emergency Contact,Notfallkontakt,
|
||||||
Emergency Contact Name,Notfall Kontaktname,
|
Emergency Contact Name,Name des Notfallkontakts,
|
||||||
Emergency Phone,Notruf,
|
Emergency Phone,Telefonnummer des Notfallkontakts,
|
||||||
ERPNext User,ERPNext Benutzer,
|
ERPNext User,ERPNext Benutzer,
|
||||||
"System User (login) ID. If set, it will become default for all HR forms.","Systembenutzer-ID (Anmeldung). Wenn gesetzt, wird sie standardmäßig für alle HR-Formulare verwendet.",
|
"System User (login) ID. If set, it will become default for all HR forms.","Systembenutzer-ID (Anmeldung). Wenn gesetzt, wird sie standardmäßig für alle HR-Formulare verwendet.",
|
||||||
Create User Permission,Benutzerberechtigung Erstellen,
|
Create User Permission,Benutzerberechtigung Erstellen,
|
||||||
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
|
This will restrict user access to other employee records,Dies schränkt den Benutzerzugriff auf andere Mitarbeiterdatensätze ein,
|
||||||
Joining Details,Details des Beitritts,
|
Joining Details,Details des Beitritts,
|
||||||
Offer Date,Angebotsdatum,
|
Offer Date,Angebotsdatum,
|
||||||
Confirmation Date,Datum bestätigen,
|
Confirmation Date,Bestätigungsdatum,
|
||||||
Contract End Date,Vertragsende,
|
Contract End Date,Vertragsende,
|
||||||
Notice (days),Meldung(s)(-Tage),
|
Notice (days),Kündigungsfrist (Tage),
|
||||||
Date Of Retirement,Zeitpunkt der Pensionierung,
|
Date Of Retirement,Zeitpunkt der Pensionierung,
|
||||||
Department and Grade,Abteilung und Klasse,
|
Department and Grade,Abteilung und Klasse,
|
||||||
Reports to,Berichte an,
|
Reports to,Vorgesetzter,
|
||||||
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
||||||
Leave Policy,Urlaubsrichtlinie,
|
Leave Policy,Urlaubsrichtlinie,
|
||||||
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
|
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
|
||||||
@@ -6545,8 +6545,8 @@ Company Email,E-Mail-Adresse des Unternehmens,
|
|||||||
Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert,
|
Provide Email Address registered in company,Geben Sie E-Mail-Adresse in Unternehmen registriert,
|
||||||
Current Address Is,Aktuelle Adresse ist,
|
Current Address Is,Aktuelle Adresse ist,
|
||||||
Current Address,Aktuelle Adresse,
|
Current Address,Aktuelle Adresse,
|
||||||
Personal Bio,Persönliches Bio,
|
Personal Bio,Lebenslauf,
|
||||||
Bio / Cover Letter,Bio / Anschreiben,
|
Bio / Cover Letter,Lebenslauf / Anschreiben,
|
||||||
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
|
Short biography for website and other publications.,Kurzbiographie für die Webseite und andere Publikationen.,
|
||||||
Passport Number,Passnummer,
|
Passport Number,Passnummer,
|
||||||
Date of Issue,Ausstellungsdatum,
|
Date of Issue,Ausstellungsdatum,
|
||||||
@@ -9533,7 +9533,7 @@ Preview Email,Vorschau E-Mail,
|
|||||||
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
|
Please select a Supplier,Bitte wählen Sie einen Lieferanten aus,
|
||||||
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
|
Supplier Lead Time (days),Vorlaufzeit des Lieferanten (Tage),
|
||||||
"Home, Work, etc.","Zuhause, Arbeit usw.",
|
"Home, Work, etc.","Zuhause, Arbeit usw.",
|
||||||
Exit Interview Held On,Beenden Sie das Interview,
|
Exit Interview Held On,Entlassungsgespräch am,
|
||||||
Condition and formula,Zustand und Formel,
|
Condition and formula,Zustand und Formel,
|
||||||
Sets 'Target Warehouse' in each row of the Items table.,Legt 'Ziellager' in jeder Zeile der Elementtabelle fest.,
|
Sets 'Target Warehouse' in each row of the Items table.,Legt 'Ziellager' in jeder Zeile der Elementtabelle fest.,
|
||||||
Sets 'Source Warehouse' in each row of the Items table.,Legt 'Source Warehouse' in jeder Zeile der Items-Tabelle fest.,
|
Sets 'Source Warehouse' in each row of the Items table.,Legt 'Source Warehouse' in jeder Zeile der Items-Tabelle fest.,
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user