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

This commit is contained in:
Deepesh Garg
2021-12-29 18:25:52 +05:30
100 changed files with 2467 additions and 1003 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
__version__ = '13.16.1' __version__ = '13.17.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -375,12 +375,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
if frappe.flags.in_test: if frappe.flags.in_test:
traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e raise e
else: else:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(message=traceback) frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process): def send_mail(deferred_process):
@@ -447,10 +448,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
if submit: if submit:
journal_entry.submit() journal_entry.submit()
frappe.db.commit()
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(message=traceback) frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True

View File

@@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document): class AccountsSettings(Document):
def on_update(self): def on_update(self):
@@ -25,6 +27,7 @@ class AccountsSettings(Document):
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields() self.toggle_discount_accounting_fields()
self.validate_pending_reposts()
def validate_stale_days(self): def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0: if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -56,3 +59,8 @@ class AccountsSettings(Document):
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)

View File

@@ -57,7 +57,8 @@ class GLEntry(Document):
# Update outstanding amt on against voucher # Update outstanding amt on against voucher
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
and self.against_voucher and self.flags.update_outstanding == 'Yes'): and self.against_voucher and self.flags.update_outstanding == 'Yes'
and not frappe.flags.is_reverse_depr_entry):
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher) self.against_voucher)

View File

@@ -57,7 +57,10 @@ class JournalEntry(AccountsController):
if not frappe.flags.in_import: if not frappe.flags.in_import:
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
self.validate_against_jv() if not frappe.flags.is_reverse_depr_entry:
self.validate_against_jv()
self.validate_stock_accounts()
self.validate_reference_doc() self.validate_reference_doc()
if self.docstatus == 0: if self.docstatus == 0:
self.set_against_account() self.set_against_account()
@@ -68,7 +71,6 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.set_account_and_party_balance() self.set_account_and_party_balance()
self.validate_inter_company_accounts() self.validate_inter_company_accounts()
self.validate_stock_accounts()
if self.docstatus == 0: if self.docstatus == 0:
self.apply_tax_withholding() self.apply_tax_withholding()

View File

@@ -171,6 +171,7 @@
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break10", "column_break10",
"amount_eligible_for_commission",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break2", "section_break2",
@@ -1561,16 +1562,23 @@
"label": "Coupon Code", "label": "Coupon Code",
"options": "Coupon Code", "options": "Coupon Code",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-08-27 20:12:57.306772", "modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",
"name_case": "Title Case", "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -46,6 +46,7 @@
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"is_free_item", "is_free_item",
"grant_commission",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -800,14 +801,22 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-04 17:34:49.924531", "modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -3,22 +3,20 @@
{% include "erpnext/public/js/controllers/accounts.js" %} {% include "erpnext/public/js/controllers/accounts.js" %}
frappe.ui.form.on("POS Profile", "onload", function(frm) {
frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
frm.set_query("tc_name", function() {
return { filters: { selling: 1 } };
});
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
});
frappe.ui.form.on('POS Profile', { frappe.ui.form.on('POS Profile', {
setup: function(frm) { setup: function(frm) {
frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
frm.set_query("tc_name", function() {
return { filters: { selling: 1 } };
});
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
frm.set_query("print_format", function() { frm.set_query("print_format", function() {
return { return {
filters: [ filters: [
@@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', {
}; };
}); });
frm.set_query("account_for_change_amount", function() { frm.set_query("account_for_change_amount", function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return { return {
filters: { filters: {
account_type: ['in', ["Cash", "Bank"]] account_type: ['in', ["Cash", "Bank"]],
is_group: 0,
company: doc.company
} }
}; };
}); });
@@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', {
}); });
frm.set_query('company_address', function(doc) { frm.set_query('company_address', function(doc) {
if(!doc.company) { if (!doc.company) {
frappe.throw(__('Please set Company')); frappe.throw(__('Please set Company'));
} }
@@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', {
}; };
}); });
frm.set_query('income_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
});
frm.set_query('cost_center', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'company': doc.company,
'is_group': 0
}
};
});
frm.set_query('expense_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
});
frm.set_query("select_print_heading", function() {
return {
filters: [
['Print Heading', 'docstatus', '!=', 2]
]
};
});
frm.set_query("write_off_account", function(doc) {
return {
filters: {
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
});
frm.set_query("write_off_cost_center", function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
if(frm.doc.company) { if (frm.doc.company) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
} }
}, },
@@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', {
frm.toggle_display('expense_account', frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company)); erpnext.is_perpetual_inventory_enabled(frm.doc.company));
} }
}) });
// Income Account
// --------------------------------
cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
};
// Cost Center
// -----------------------------
cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'company': doc.company,
'is_group': 0
}
};
};
// Expense Account
// -----------------------------
cur_frm.fields_dict["expense_account"].get_query = function(doc) {
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
};
// ------------------ Get Print Heading ------------------------------------
cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
return{
filters:[
['Print Heading', 'docstatus', '!=', 2]
]
};
};
cur_frm.fields_dict.write_off_account.get_query = function(doc) {
return{
filters:{
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
};
// Write off cost center
// -----------------------
cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
return{
filters:{
'is_group': 0,
'company': doc.company
}
};
};

View File

@@ -112,6 +112,9 @@ class PurchaseInvoice(BuyingController):
self.set_status() self.set_status()
self.validate_purchase_receipt_if_update_stock() self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self): def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date): if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@@ -292,8 +295,15 @@ class PurchaseInvoice(BuyingController):
item.expense_account = stock_not_billed_account item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company) company = self.company)
if not asset_category_account:
form_link = get_link_to_form('Asset Category', asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account")
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail: elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate: elif not item.expense_account and for_validate:

View File

@@ -182,6 +182,7 @@
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break10", "column_break10",
"amount_eligible_for_commission",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break2", "section_break2",
@@ -2019,6 +2020,12 @@
"label": "Total Billing Hours", "label": "Total Billing Hours",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-10-11 20:19:38.667508", "modified": "2021-10-21 20:19:38.667508",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -36,7 +36,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_disposal_account_and_cost_center, get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain, get_gl_entries_on_asset_regain,
post_depreciation_entries, make_depreciation_entry,
) )
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -157,6 +157,8 @@ class SalesInvoice(SellingController):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points) validate_loyalty_points(self, self.loyalty_points)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_fixed_asset(self): def validate_fixed_asset(self):
for d in self.get("items"): for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@@ -943,6 +945,7 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
if asset.calculate_depreciation: if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_sale(asset)
self.reset_depreciation_schedule(asset) self.reset_depreciation_schedule(asset)
else: else:
@@ -1006,22 +1009,20 @@ class SalesInvoice(SellingController):
def depreciate_asset(self, asset): def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(self.posting_date) asset.prepare_depreciation_data(date_of_sale=self.posting_date)
asset.save() asset.save()
post_depreciation_entries(self.posting_date) make_depreciation_entry(asset.name, self.posting_date)
def reset_depreciation_schedule(self, asset): def reset_depreciation_schedule(self, asset):
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset # recreate original depreciation schedule of the asset
asset.prepare_depreciation_data() asset.prepare_depreciation_data(date_of_return=self.posting_date)
self.modify_depreciation_schedule_for_asset_repairs(asset) self.modify_depreciation_schedule_for_asset_repairs(asset)
asset.save() asset.save()
self.delete_depreciation_entry_made_after_sale(asset)
def modify_depreciation_schedule_for_asset_repairs(self, asset): def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all( asset_repairs = frappe.get_all(
'Asset Repair', 'Asset Repair',
@@ -1035,7 +1036,7 @@ class SalesInvoice(SellingController):
asset_repair.modify_depreciation_schedule() asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data() asset.prepare_depreciation_data()
def delete_depreciation_entry_made_after_sale(self, asset): def reverse_depreciation_entry_made_after_sale(self, asset):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
@@ -1050,11 +1051,21 @@ class SalesInvoice(SellingController):
row += 1 row += 1
if schedule.schedule_date == posting_date_of_original_invoice: if schedule.schedule_date == posting_date_of_original_invoice:
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice): if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \
or self.sale_happens_in_the_future(posting_date_of_original_invoice):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate() reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit() reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
def get_posting_date_of_sales_invoice(self): def get_posting_date_of_sales_invoice(self):
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
@@ -1069,6 +1080,18 @@ class SalesInvoice(SellingController):
return True return True
return False return False
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
if posting_date_of_original_invoice > getdate():
return True
return False
def get_depreciation_amount_in_je(self, journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
@property @property
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):

View File

@@ -2204,9 +2204,9 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0) enable_discount_accounting(enable=0)
def test_asset_depreciation_on_sale(self): def test_asset_depreciation_on_sale_with_pro_rata(self):
""" """
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30. Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
""" """
create_asset_data() create_asset_data()
@@ -2219,7 +2219,7 @@ class TestSalesInvoice(unittest.TestCase):
expected_values = [ expected_values = [
["2020-06-30", 1311.48, 1311.48], ["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.48], ["2021-06-30", 20000.0, 21311.48],
["2021-09-30", 3966.76, 25278.24] ["2021-09-30", 5041.1, 26352.58]
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(asset.schedules):
@@ -2228,6 +2228,59 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry) self.assertTrue(schedule.journal_entry)
def test_asset_depreciation_on_sale_without_pro_rata(self):
"""
Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale.
"""
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1,
available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3,
expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1)
post_depreciation_entries(getdate("2021-09-30"))
create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31"))
asset.load_from_db()
expected_values = [
["2020-12-31", 30000, 30000],
["2021-12-31", 30000, 60000]
]
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry)
def test_depreciation_on_return_of_sold_asset(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
post_depreciation_entries(getdate("2021-09-30"))
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
return_si = make_return_doc("Sales Invoice", si.name)
return_si.submit()
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48, True],
["2021-06-30", 20000.0, 21311.48, True],
["2022-06-30", 20000.0, 41311.48, False],
["2023-06-30", 20000.0, 61311.48, False],
["2024-06-30", 20000.0, 81311.48, False],
["2025-06-06", 18688.52, 100000.0, False]
]
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
def test_sales_invoice_against_supplier(self): def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer, make_customer,
@@ -2317,6 +2370,29 @@ class TestSalesInvoice(unittest.TestCase):
si.reload() si.reload()
self.assertEqual(si.status, "Paid") self.assertEqual(si.status, "Paid")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
item = copy.deepcopy(si.get('items')[0])
item.update({
"qty": 1,
"rate": 500,
"grant_commission": 1
})
si.append("items", item)
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
si.commission_rate = commission_rate
si.save()
self.assertEqual(si.amount_eligible_for_commission, 500)
self.assertEqual(si.total_commission, total_commission)
# Test invalid values
for commission_rate in (101, -1):
si.reload()
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
def test_sales_invoice_submission_post_account_freezing_date(self): def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True) si = create_sales_invoice(do_not_save=True)

View File

@@ -47,6 +47,7 @@
"pricing_rules", "pricing_rules",
"stock_uom_rate", "stock_uom_rate",
"is_free_item", "is_free_item",
"grant_commission",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -745,7 +746,6 @@
"fieldname": "asset", "fieldname": "asset",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Asset", "label": "Asset",
"no_copy": 1,
"options": "Asset" "options": "Asset"
}, },
{ {
@@ -829,15 +829,23 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",
"options": "Account" "options": "Account"
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-19 13:41:53.435827", "modified": "2021-10-05 12:24:54.968907",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -109,7 +109,11 @@ class ReceivablePayableReport(object):
invoiced = 0.0, invoiced = 0.0,
paid = 0.0, paid = 0.0,
credit_note = 0.0, credit_note = 0.0,
outstanding = 0.0 outstanding = 0.0,
invoiced_in_account_currency = 0.0,
paid_in_account_currency = 0.0,
credit_note_in_account_currency = 0.0,
outstanding_in_account_currency = 0.0
) )
self.get_invoices(gle) self.get_invoices(gle)
@@ -150,21 +154,28 @@ class ReceivablePayableReport(object):
# gle_balance will be the total "debit - credit" for receivable type reports and # gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports # and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle) gle_balance = self.get_gle_balance(gle)
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
if gle_balance > 0: if gle_balance > 0:
if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
# debit against sales / purchase invoice # debit against sales / purchase invoice
row.paid -= gle_balance row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
else: else:
# invoice # invoice
row.invoiced += gle_balance row.invoiced += gle_balance
row.invoiced_in_account_currency += gle_balance_in_account_currency
else: else:
# payment or credit note for receivables # payment or credit note for receivables
if self.is_invoice(gle): if self.is_invoice(gle):
# stand alone debit / credit note # stand alone debit / credit note
row.credit_note -= gle_balance row.credit_note -= gle_balance
row.credit_note_in_account_currency -= gle_balance_in_account_currency
else: else:
# advance / unlinked payment or other adjustment # advance / unlinked payment or other adjustment
row.paid -= gle_balance row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
if gle.cost_center: if gle.cost_center:
row.cost_center = str(gle.cost_center) row.cost_center = str(gle.cost_center)
@@ -216,8 +227,13 @@ class ReceivablePayableReport(object):
# as we can use this to filter out invoices without outstanding # as we can use this to filter out invoices without outstanding
for key, row in self.voucher_balance.items(): for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
row.credit_note_in_account_currency, self.currency_precision)
row.invoice_grand_total = row.invoiced row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
(abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
# non-zero oustanding, we must consider this row # non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms: if self.is_invoice(row) and self.filters.based_on_payment_terms:
@@ -529,7 +545,9 @@ class ReceivablePayableReport(object):
def set_ageing(self, row): def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date": if self.filters.ageing_based_on == "Due Date":
entry_date = row.due_date # use posting date as a fallback for advances posted via journal and payment entry
# when ageing viewed by due date
entry_date = row.due_date or row.posting_date
elif self.filters.ageing_based_on == "Supplier Invoice Date": elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date entry_date = row.bill_date
else: else:
@@ -583,12 +601,14 @@ class ReceivablePayableReport(object):
else: else:
select_fields = "debit, credit" select_fields = "debit, credit"
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
remarks = ", remarks" if self.filters.get("show_remarks") else "" remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql(""" self.gl_entries = frappe.db.sql("""
select select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0} {remarks} against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from from
`tabGL Entry` `tabGL Entry`
where where
@@ -596,8 +616,8 @@ class ReceivablePayableReport(object):
and is_cancelled = 0 and is_cancelled = 0
and party_type=%s and party_type=%s
and (party is not null and party != '') and (party is not null and party != '')
{1} {2} {3}""" {2} {3} {4}"""
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self): def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"): if self.filters.get("sales_person"):
@@ -718,6 +738,13 @@ class ReceivablePayableReport(object):
# get the balance of the GL (debit - credit) or reverse balance based on report type # get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
def get_gle_balance_in_account_currency(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
def get_reverse_balance_in_account_currency(self, gle):
return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
def get_reverse_balance(self, gle): def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa # get "credit" balance if report type is "debit" and vice versa
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')

View File

@@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Include Default Book Entries"), "label": __("Include Default Book Entries"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 1 "default": 1
},
{
"fieldname": "show_zero_values",
"label": __("Show zero values"),
"fieldtype": "Check"
} }
], ],
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {

View File

@@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import (
get_cash_flow_accounts, get_cash_flow_accounts,
) )
from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary
from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
get_fiscal_year_data,
sort_accounts,
)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_chart_data as get_pl_chart_data, get_chart_data as get_pl_chart_data,
) )
@@ -265,7 +269,7 @@ def get_columns(companies, filters):
return columns return columns
def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False):
accounts, accounts_by_name = get_account_heads(root_type, accounts, accounts_by_name, parent_children_map = get_account_heads(root_type,
companies, filters) companies, filters)
if not accounts: return [] if not accounts: return []
@@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
if out: if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency) add_total_row(out, root_type, balance_must_be, companies, company_currency)
@@ -364,13 +370,13 @@ def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, filters)
if not accounts: if not accounts:
return None, None return None, None, None
accounts = update_parent_account_names(accounts) accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name return accounts, accounts_by_name, parent_children_map
def update_parent_account_names(accounts): def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list. """Update parent_account_name in accounts list.

View File

@@ -1,18 +1,21 @@
{ {
"add_total_row": 0, "add_total_row": 0,
"apply_user_permissions": 1, "columns": [],
"creation": "2013-05-06 12:28:23", "creation": "2013-05-06 12:28:23",
"disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [],
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2017-03-06 05:52:57.645281", "modified": "2021-10-06 06:26:07.881340",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Partners Commission", "name": "Sales Partners Commission",
"owner": "Administrator", "owner": "Administrator",
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", "prepared_report": 0,
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
"ref_doctype": "Sales Invoice", "ref_doctype": "Sales Invoice",
"report_name": "Sales Partners Commission", "report_name": "Sales Partners Commission",
"report_type": "Query Report", "report_type": "Query Report",

View File

@@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date posting_date = entry.posting_date
voucher_type = entry.voucher_type voucher_type = entry.voucher_type
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts: if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit) tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit)
if rate and tds_deducted: if tds_deducted:
row = { row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name') 'supplier': supplier_map.get(supplier, {}).get('name')
@@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_supplier_pan_map(): def get_supplier_pan_map():
supplier_map = frappe._dict() supplier_map = frappe._dict()
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name']) suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
for d in suppliers: for d in suppliers:
supplier_map[d.name] = d supplier_map[d.name] = d

View File

@@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus==1) { if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() { frm.add_custom_button(__("Transfer Asset"), function() {
erpnext.asset.transfer_asset(frm); erpnext.asset.transfer_asset(frm);
}, __("Manage")); }, __("Manage"));
frm.add_custom_button("Scrap Asset", function() { frm.add_custom_button(__("Scrap Asset"), function() {
erpnext.asset.scrap_asset(frm); erpnext.asset.scrap_asset(frm);
}, __("Manage")); }, __("Manage"));
frm.add_custom_button("Sell Asset", function() { frm.add_custom_button(__("Sell Asset"), function() {
frm.trigger("make_sales_invoice"); frm.trigger("make_sales_invoice");
}, __("Manage")); }, __("Manage"));
} else if (frm.doc.status=='Scrapped') { } else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() { frm.add_custom_button(__("Restore Asset"), function() {
erpnext.asset.restore_asset(frm); erpnext.asset.restore_asset(frm);
}, __("Manage")); }, __("Manage"));
} }
@@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
if (frm.doc.status != 'Fully Depreciated') { if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() { frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment"); frm.trigger("create_asset_value_adjustment");
}, __("Manage")); }, __("Manage"));
} }
@@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', {
} }
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button("View General Ledger", function() { frm.add_custom_button(__("View General Ledger"), function() {
frappe.route_options = { frappe.route_options = {
"voucher_no": frm.doc.name, "voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date, "from_date": frm.doc.available_for_use_date,
@@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
}); });
}, },
create_asset_adjustment: function(frm) { create_asset_value_adjustment: function(frm) {
frappe.call({ frappe.call({
args: { args: {
"asset": frm.doc.name, "asset": frm.doc.name,
"asset_category": frm.doc.asset_category, "asset_category": frm.doc.asset_category,
"company": frm.doc.company "company": frm.doc.company
}, },
method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
freeze: 1, freeze: 1,
callback: function(r) { callback: function(r) {
var doclist = frappe.model.sync(r.message); var doclist = frappe.model.sync(r.message);

View File

@@ -73,12 +73,12 @@ class Asset(AccountsController):
if self.is_existing_asset and self.purchase_invoice: if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
def prepare_depreciation_data(self, date_of_sale=None): def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
if self.calculate_depreciation: if self.calculate_depreciation:
self.value_after_depreciation = 0 self.value_after_depreciation = 0
self.set_depreciation_rate() self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_sale) self.make_depreciation_schedule(date_of_sale)
self.set_accumulated_depreciation(date_of_sale) self.set_accumulated_depreciation(date_of_sale, date_of_return)
else: else:
self.finance_books = [] self.finance_books = []
self.value_after_depreciation = (flt(self.gross_purchase_amount) - self.value_after_depreciation = (flt(self.gross_purchase_amount) -
@@ -180,7 +180,7 @@ class Asset(AccountsController):
d.precision("rate_of_depreciation")) d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self, date_of_sale): def make_depreciation_schedule(self, date_of_sale):
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'):
self.schedules = [] self.schedules = []
if not self.available_for_use_date: if not self.available_for_use_date:
@@ -193,8 +193,7 @@ class Asset(AccountsController):
# value_after_depreciation - current Asset value # value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation: if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) - value_after_depreciation = flt(d.value_after_depreciation)
flt(self.opening_accumulated_depreciation))
else: else:
value_after_depreciation = (flt(self.gross_purchase_amount) - value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation)) flt(self.opening_accumulated_depreciation))
@@ -230,17 +229,19 @@ class Asset(AccountsController):
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date, date_of_sale) from_date, date_of_sale)
self.append("schedules", { if depreciation_amount > 0:
"schedule_date": date_of_sale, self.append("schedules", {
"depreciation_amount": depreciation_amount, "schedule_date": date_of_sale,
"depreciation_method": d.depreciation_method, "depreciation_amount": depreciation_amount,
"finance_book": d.finance_book, "depreciation_method": d.depreciation_method,
"finance_book_id": d.idx "finance_book": d.finance_book,
}) "finance_book_id": d.idx
})
break break
# For first row # For first row
if has_pro_rata and n==0: if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date) self.available_for_use_date, d.depreciation_start_date)
@@ -253,13 +254,17 @@ class Asset(AccountsController):
if not self.flags.increase_in_asset_life: if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date, self.to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation)) (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, schedule_date, self.to_date) depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days) schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date last_schedule_date = schedule_date
@@ -349,7 +354,12 @@ class Asset(AccountsController):
# if it returns True, depreciation_amount will not be equal for the first and last rows # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row): def check_is_pro_rata(self, row):
has_pro_rata = False has_pro_rata = False
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = self.get_modified_available_for_use_date(row)
days = date_diff(row.depreciation_start_date, from_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365 # if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
@@ -359,6 +369,9 @@ class Asset(AccountsController):
return has_pro_rata return has_pro_rata
def get_modified_available_for_use_date(self, row):
return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation))
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@@ -395,7 +408,29 @@ class Asset(AccountsController):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date") frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
.format(row.idx)) .format(row.idx))
def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False): # to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
if not self.opening_accumulated_depreciation:
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
return depreciation_amount_for_last_row
def get_depreciation_amount_for_first_row(self, finance_book):
if self.has_only_one_finance_book():
return self.schedules[0].depreciation_amount
else:
for schedule in self.schedules:
if schedule.finance_book == finance_book:
return schedule.depreciation_amount
def has_only_one_finance_book(self):
if len(self.finance_books) == 1:
return True
def set_accumulated_depreciation(self, date_of_sale=None, date_of_return=None, ignore_booked_entry = False):
straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line'] straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line']
finance_books = [] finance_books = []
@@ -412,7 +447,7 @@ class Asset(AccountsController):
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line # for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale: if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale and not date_of_return:
book = self.get('finance_books')[cint(d.finance_book_id) - 1] book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation - depreciation_amount += flt(value_after_depreciation -
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))
@@ -435,7 +470,6 @@ class Asset(AccountsController):
asset_value_after_full_schedule = flt( asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) - flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation) -
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount')) flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
if (row.expected_value_after_useful_life and if (row.expected_value_after_useful_life and
@@ -697,14 +731,14 @@ def create_asset_repair(asset, asset_name):
return asset_repair return asset_repair
@frappe.whitelist() @frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company): def create_asset_value_adjustment(asset, asset_category, company):
asset_maintenance = frappe.get_doc("Asset Value Adjustment") asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
asset_maintenance.update({ asset_value_adjustment.update({
"asset": asset, "asset": asset,
"company": company, "company": company,
"asset_category": asset_category "asset_category": asset_category
}) })
return asset_maintenance return asset_value_adjustment
@frappe.whitelist() @frappe.whitelist()
def transfer_asset(args): def transfer_asset(args):
@@ -826,13 +860,11 @@ def get_total_days(date, frequency):
@erpnext.allow_regional @erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row): def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life: if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(row.value_after_depreciation) - depreciation_amount = (flt(asset.gross_purchase_amount) -
flt(row.expected_value_after_useful_life)) / depreciation_left flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair # if the Depreciation Schedule is being modified after Asset Repair
else: else:

View File

@@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None):
je.finance_book = d.finance_book je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
credit_entry = { credit_entry = {
"account": accumulated_depreciation_account, "account": credit_account,
"credit_in_account_currency": d.depreciation_amount, "credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "reference_name": asset.name,
@@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None):
} }
debit_entry = { debit_entry = {
"account": depreciation_expense_account, "account": debit_account,
"debit_in_account_currency": d.depreciation_amount, "debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "reference_name": asset.name,
@@ -132,6 +134,20 @@ def get_depreciation_accounts(asset):
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
if root_type == "Expense":
credit_account = accumulated_depreciation_account
debit_account = depreciation_expense_account
elif root_type == "Income":
credit_account = depreciation_expense_account
debit_account = accumulated_depreciation_account
else:
frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
return credit_account, debit_account
@frappe.whitelist() @frappe.whitelist()
def scrap_asset(asset_name): def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
return { return {
"filters": { "filters": {
"root_type": "Expense", "root_type": ["in", ["Expense", "Income"]],
"is_group": 0, "is_group": 0,
"company": d.company_name "company": d.company_name
} }

View File

@@ -42,10 +42,10 @@ class AssetCategory(Document):
def validate_account_types(self): def validate_account_types(self):
account_type_map = { account_type_map = {
'fixed_asset_account': { 'account_type': 'Fixed Asset' }, 'fixed_asset_account': {'account_type': ['Fixed Asset']},
'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
'depreciation_expense_account': { 'root_type': 'Expense' }, 'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
} }
for d in self.accounts: for d in self.accounts:
for fieldname in account_type_map.keys(): for fieldname in account_type_map.keys():
@@ -53,11 +53,11 @@ class AssetCategory(Document):
selected_account = d.get(fieldname) selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
expected_key_type = account_type_map[fieldname][key_to_match] expected_key_types = account_type_map[fieldname][key_to_match]
if selected_key_type != expected_key_type: if selected_key_type not in expected_key_types:
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
title=_("Invalid Account")) title=_("Invalid Account"))
def valide_cwip_account(self): def valide_cwip_account(self):

View File

@@ -60,6 +60,10 @@ frappe.ui.form.on('Asset Repair', {
if (frm.doc.repair_status == "Completed") { if (frm.doc.repair_status == "Completed") {
frm.set_value('completion_date', frappe.datetime.now_datetime()); frm.set_value('completion_date', frappe.datetime.now_datetime());
} }
},
stock_items_on_form_rendered() {
erpnext.setup_serial_or_batch_no();
} }
}); });

View File

@@ -118,9 +118,10 @@ class AssetRepair(AccountsController):
for stock_item in self.get('stock_items'): for stock_item in self.get('stock_items'):
stock_entry.append('items', { stock_entry.append('items', {
"s_warehouse": self.warehouse, "s_warehouse": self.warehouse,
"item_code": stock_item.item, "item_code": stock_item.item_code,
"qty": stock_item.consumed_quantity, "qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate "basic_rate": stock_item.valuation_rate,
"serial_no": stock_item.serial_no
}) })
stock_entry.insert() stock_entry.insert()

View File

@@ -11,12 +11,15 @@ from erpnext.assets.doctype.asset.test_asset import (
create_asset_data, create_asset_data,
set_depreciation_settings_in_company, set_depreciation_settings_in_company,
) )
from erpnext.stock.doctype.item.test_item import create_item
class TestAssetRepair(unittest.TestCase): class TestAssetRepair(unittest.TestCase):
def setUp(self): @classmethod
def setUpClass(cls):
set_depreciation_settings_in_company() set_depreciation_settings_in_company()
create_asset_data() create_asset_data()
create_item("_Test Stock Item")
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self): def test_update_status(self):
@@ -70,9 +73,28 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(stock_entry.stock_entry_type, "Material Issue") self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code)
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
def test_serialized_item_consumption(self):
from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
stock_entry = make_serialized_item()
serial_nos = stock_entry.get("items")[0].serial_no
serial_no = serial_nos.split("\n")[0]
# should not raise any error
create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code,
warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1)
# should raise error
asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC",
item_code = stock_entry.get("items")[0].item_code)
asset_repair.repair_status = "Completed"
self.assertRaises(SerialNoRequiredError, asset_repair.submit)
def test_increase_in_asset_value_due_to_stock_consumption(self): def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1, submit=1) asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset) initial_asset_value = get_asset_value(asset)
@@ -137,11 +159,12 @@ def create_asset_repair(**args):
if args.stock_consumption: if args.stock_consumption:
asset_repair.stock_consumption = 1 asset_repair.stock_consumption = 1
asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company)
asset_repair.append("stock_items", { asset_repair.append("stock_items", {
"item": args.item or args.item_code or "_Test Item", "item_code": args.item_code or "_Test Stock Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100, "valuation_rate": args.rate if args.get("rate") is not None else 100,
"consumed_quantity": args.qty or 1 "consumed_quantity": args.qty or 1,
"serial_no": args.serial_no
}) })
asset_repair.insert(ignore_if_duplicate=True) asset_repair.insert(ignore_if_duplicate=True)
@@ -158,7 +181,7 @@ def create_asset_repair(**args):
}) })
stock_entry.append('items', { stock_entry.append('items', {
"t_warehouse": asset_repair.warehouse, "t_warehouse": asset_repair.warehouse,
"item_code": asset_repair.stock_items[0].item, "item_code": asset_repair.stock_items[0].item_code,
"qty": asset_repair.stock_items[0].consumed_quantity "qty": asset_repair.stock_items[0].consumed_quantity
}) })
stock_entry.submit() stock_entry.submit()

View File

@@ -5,19 +5,13 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"item", "item_code",
"valuation_rate", "valuation_rate",
"consumed_quantity", "consumed_quantity",
"total_value" "total_value",
"serial_no"
], ],
"fields": [ "fields": [
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{ {
"fetch_from": "item.valuation_rate", "fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate", "fieldname": "valuation_rate",
@@ -38,12 +32,24 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Total Value", "label": "Total Value",
"read_only": 1 "read_only": 1
},
{
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No"
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-12 03:19:55.006300", "modified": "2021-11-11 18:23:00.492483",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair Consumed Item", "name": "Asset Repair Consumed Item",

View File

@@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController):
self.create_raw_materials_supplied("supplied_items") self.create_raw_materials_supplied("supplied_items")
self.set_received_qty_for_drop_ship_items() self.set_received_qty_for_drop_ship_items()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference) validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self): def validate_with_previous_doc(self):
super(PurchaseOrder, self).validate_with_previous_doc({ super(PurchaseOrder, self).validate_with_previous_doc({

View File

@@ -17,6 +17,7 @@
"company", "company",
"transaction_date", "transaction_date",
"valid_till", "valid_till",
"quotation_number",
"amended_from", "amended_from",
"address_section", "address_section",
"supplier_address", "supplier_address",
@@ -797,6 +798,11 @@
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"label": "Valid Till" "label": "Valid Till"
},
{
"fieldname": "quotation_number",
"fieldtype": "Data",
"label": "Quotation Number"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
@@ -804,10 +810,11 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 00:58:20.995491", "modified": "2021-12-11 06:43:20.924080",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -0,0 +1,33 @@
# Version 13.17.0 Release Notes
### Features & Enhancements
- Provision to consumed the serialized raw materials during Asset Repairs ([#28349](https://github.com/frappe/erpnext/pull/28349))
- Grant commission on certain items ([#27467](https://github.com/frappe/erpnext/pull/27467))
- KSA E-Invoing optimizations and POS support ([#28799](https://github.com/frappe/erpnext/pull/28799))
- Added QI link in Job Card Dashboard ([#28643](https://github.com/frappe/erpnext/pull/28643))
- Show Zero Values filter in consolidated financial statement ([#28636](https://github.com/frappe/erpnext/pull/28636))
- Validate pending reposts before freezing stock/account ([#28815](https://github.com/frappe/erpnext/pull/28815))
### Fixes
- Mapping to maintenance visit gets erased ([#28917](https://github.com/frappe/erpnext/pull/28917))
- Ignore mandatory fields while creating WO from SO ([#28772](https://github.com/frappe/erpnext/pull/28772))
- Map serial no from schedule if only one ([#28745](https://github.com/frappe/erpnext/pull/28745))
- TDS Monthly payable report ([#28764](https://github.com/frappe/erpnext/pull/28764))
- Maintenance Visit purposes tables is not visible on submission ([#28792](https://github.com/frappe/erpnext/pull/28792))
- fetch memberships for 80G certificate by from date only ([#28700](https://github.com/frappe/erpnext/pull/28700))
- Do not add GST fields if company is not from India ([#28592](https://github.com/frappe/erpnext/pull/28592))
- Wrong german translation of abbreviation: PAN ([#28802](https://github.com/frappe/erpnext/pull/28802))
- Removed attachment limit from item doctype ([#28632](https://github.com/frappe/erpnext/pull/28632))
- Better Error logging for deferred revenue/expense booking ([#28731](https://github.com/frappe/erpnext/pull/28731))
- Create Depreciation Schedules for existing Assets accurately ([#28675](https://github.com/frappe/erpnext/pull/28675))
- Incorrect hsn-wise summary if the invoice has repeated item code ([#28783](https://github.com/frappe/erpnext/pull/28783))
- Paid invoices showing in Accounts Receivable /Accounts Payable report ([#28627](https://github.com/frappe/erpnext/pull/28627))
- Shipping Rule picking up old net_rate ([#28302](https://github.com/frappe/erpnext/pull/28302))
- Actual tax conversion in case of multicurrency invoices ([#28539](https://github.com/frappe/erpnext/pull/28539))
- QRCode for invoices with special characters ([#28715](https://github.com/frappe/erpnext/pull/28715))
- Incorrect outgoing rates when "Allow Continuous Material Consumption" enabled in manufacturing settings ([#28710](https://github.com/frappe/erpnext/pull/28710))
- Misleading "Set Default Warehouse" fields after saving ([#28798](https://github.com/frappe/erpnext/pull/28798))
- Taxjar Nexus list visible only if child table is visible ([#28656](https://github.com/frappe/erpnext/pull/28656))

View File

@@ -256,7 +256,12 @@ class AccountsController(TransactionBase):
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
calculate_taxes_and_totals(self) calculate_taxes_and_totals(self)
if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: if self.doctype in (
'Sales Order',
'Delivery Note',
'Sales Invoice',
'POS Invoice',
):
self.calculate_commission() self.calculate_commission()
self.calculate_contribution() self.calculate_contribution()

View File

@@ -120,13 +120,27 @@ class SellingController(StockController):
self.in_words = money_in_words(amount, self.currency) self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self): def calculate_commission(self):
if self.meta.get_field("commission_rate"): if not self.meta.get_field("commission_rate"):
self.round_floats_in(self, ["base_net_total", "commission_rate"]) return
if self.commission_rate > 100.0:
throw(_("Commission rate cannot be greater than 100"))
self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0, self.round_floats_in(
self.precision("total_commission")) self, ("amount_eligible_for_commission", "commission_rate")
)
if not (0 <= self.commission_rate <= 100.0):
throw("{} {}".format(
_(self.meta.get_label("commission_rate")),
_("must be between 0 and 100"),
))
self.amount_eligible_for_commission = sum(
item.base_net_amount for item in self.items if item.grant_commission
)
self.total_commission = flt(
self.amount_eligible_for_commission * self.commission_rate / 100.0,
self.precision("total_commission")
)
def calculate_contribution(self): def calculate_contribution(self):
if not self.meta.get_field("sales_team"): if not self.meta.get_field("sales_team"):
@@ -138,7 +152,7 @@ class SellingController(StockController):
self.round_floats_in(sales_person) self.round_floats_in(sales_person)
sales_person.allocated_amount = flt( sales_person.allocated_amount = flt(
self.base_net_total * sales_person.allocated_percentage / 100.0, self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person)) self.precision("allocated_amount", sales_person))
if sales_person.commission_rate: if sales_person.commission_rate:

View File

@@ -0,0 +1,22 @@
import unittest
import frappe
class TestUtils(unittest.TestCase):
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)

View File

@@ -6,7 +6,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import comma_and, flt, unique from frappe.utils import comma_and, flt, unique
from erpnext.e_commerce.redisearch import ( from erpnext.e_commerce.redisearch_utils import (
create_website_items_index, create_website_items_index,
get_indexable_web_fields, get_indexable_web_fields,
is_search_module_loaded, is_search_module_loaded,

View File

@@ -11,7 +11,7 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
from erpnext.e_commerce.redisearch import ( from erpnext.e_commerce.redisearch_utils import (
delete_item_from_index, delete_item_from_index,
insert_item_to_index, insert_item_to_index,
update_index_for_item, update_index_for_item,

View File

@@ -20,7 +20,6 @@
"configuration_cb", "configuration_cb",
"shipping_account_head", "shipping_account_head",
"section_break_12", "section_break_12",
"nexus_address",
"nexus" "nexus"
], ],
"fields": [ "fields": [
@@ -87,15 +86,11 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "nexus",
"fieldname": "section_break_12", "fieldname": "section_break_12",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Nexus List" "label": "Nexus List"
}, },
{
"fieldname": "nexus_address",
"fieldtype": "HTML",
"label": "Nexus Address"
},
{ {
"fieldname": "nexus", "fieldname": "nexus",
"fieldtype": "Table", "fieldtype": "Table",
@@ -107,20 +102,20 @@
"fieldname": "configuration_cb", "fieldname": "configuration_cb",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company" "options": "Company"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-11-08 18:02:29.232090", "modified": "2021-11-30 12:17:24.647979",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "TaxJar Settings", "name": "TaxJar Settings",

View File

@@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client
class TaxJarSettings(Document): class TaxJarSettings(Document):
def on_update(self): def on_update(self):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") TAXJAR_SANDBOX_MODE = self.is_sandbox
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'}) fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden') fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')

View File

@@ -273,6 +273,9 @@ doc_events = {
"erpnext.regional.india.utils.update_taxable_values" "erpnext.regional.india.utils.update_taxable_values"
] ]
}, },
"POS Invoice": {
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
},
"Purchase Invoice": { "Purchase Invoice": {
"validate": [ "validate": [
"erpnext.regional.india.utils.validate_reverse_charge_transaction", "erpnext.regional.india.utils.validate_reverse_charge_transaction",

View File

@@ -96,15 +96,8 @@ class Employee(NestedSet):
'user': self.user_id 'user': self.user_id
}) })
if employee_user_permission_exists: return if employee_user_permission_exists:
return
employee_user_permission_exists = frappe.db.exists('User Permission', {
'allow': 'Employee',
'for_value': self.name,
'user': self.user_id
})
if employee_user_permission_exists: return
add_user_permission("Employee", self.name, self.user_id) add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id) set_user_permission_if_allowed("Company", self.company, self.user_id)

View File

@@ -323,10 +323,14 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
target.maintenance_schedule = source.name target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id target.maintenance_schedule_detail = s_id
def update_sales(source, target, parent): def update_sales_and_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person target.service_person = sales_person
target.serial_no = '' serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
else:
target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, { doclist = get_mapped_doc("Maintenance Schedule", source_name, {
"Maintenance Schedule": { "Maintenance Schedule": {
@@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"Maintenance Schedule Item": { "Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose", "doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name, "condition": lambda doc: doc.item_name == item_name,
"postprocess": update_sales "postprocess": update_sales_and_serial
} }
}, target_doc) }, target_doc)

View File

@@ -56,9 +56,14 @@ class TestMaintenanceSchedule(unittest.TestCase):
ms.submit() ms.submit()
s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1]) s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
# Check if item is mapped in visit.
test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
self.assertEqual(len(test_map_visit.purposes), 1)
self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item")
visit = frappe.new_doc('Maintenance Visit') visit = frappe.new_doc('Maintenance Visit')
visit = test visit = test_map_visit
visit.maintenance_schedule = ms.name visit.maintenance_schedule = ms.name
visit.maintenance_schedule_detail = s_id visit.maintenance_schedule_detail = s_id
visit.completion_status = "Partially Completed" visit.completion_status = "Partially Completed"

View File

@@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', {
} }
}); });
} }
else {
frm.clear_table("purposes");
}
if (!frm.doc.status) { if (!frm.doc.status) {
frm.set_value({ status: 'Draft' }); frm.set_value({ status: 'Draft' });
} }
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() }); frm.set_value({ mntc_date: frappe.datetime.get_today() });
} }
}, },

View File

@@ -5,10 +5,17 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'job_card', 'fieldname': 'job_card',
'non_standard_fieldnames': {
'Quality Inspection': 'reference_name'
},
'transactions': [ 'transactions': [
{ {
'label': _('Transactions'), 'label': _('Transactions'),
'items': ['Material Request', 'Stock Entry'] 'items': ['Material Request', 'Stock Entry']
},
{
'label': _('Reference'),
'items': ['Quality Inspection']
} }
] ]
} }

View File

@@ -178,6 +178,7 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list
erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.set_default_payroll_based_on
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
@@ -292,7 +293,7 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.create_gst_payment_entry_fields erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items erpnext.patches.v13_0.update_amt_in_work_order_required_items
@@ -304,14 +305,13 @@ erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category") execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.create_website_items #30-09-2021 erpnext.patches.v13_0.create_website_items #30-09-2021
erpnext.patches.v13_0.populate_e_commerce_settings erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items erpnext.patches.v13_0.make_homepage_products_website_items
@@ -335,3 +335,5 @@ erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others #16-12-2021

View File

@@ -9,24 +9,29 @@ def execute():
frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
frappe.reload_doc('accounts', 'doctype', 'payment_entry') frappe.reload_doc('accounts', 'doctype', 'payment_entry')
custom_fields = { if frappe.db.exists('Company', {'country': 'India'}):
'Payment Entry': [ custom_fields = {
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', 'Payment Entry': [
print_hide=1, collapsible=1), dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', print_hide=1, collapsible=1),
print_hide=1, options='Address'), dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
dict(fieldname='company_gstin', label='Company GSTIN', print_hide=1, options='Address'),
fieldtype='Data', insert_after='company_address', dict(fieldname='company_gstin', label='Company GSTIN',
fetch_from='company_address.gstin', print_hide=1, read_only=1), fieldtype='Data', insert_after='company_address',
dict(fieldname='place_of_supply', label='Place of Supply', fetch_from='company_address.gstin', print_hide=1, read_only=1),
fieldtype='Data', insert_after='company_gstin', dict(fieldname='place_of_supply', label='Place of Supply',
print_hide=1, read_only=1), fieldtype='Data', insert_after='company_gstin',
dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', print_hide=1, read_only=1),
print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
dict(fieldname='customer_gstin', label='Customer GSTIN', print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
fieldtype='Data', insert_after='customer_address', dict(fieldname='customer_gstin', label='Customer GSTIN',
fetch_from='customer_address.gstin', print_hide=1, read_only=1) fieldtype='Data', insert_after='customer_address',
] fetch_from='customer_address.gstin', print_hide=1, read_only=1)
} ]
}
create_custom_fields(custom_fields, update=True) create_custom_fields(custom_fields, update=True)
else:
fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin']
for field in fields:
frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}")

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2020, Wahni Green Technologies and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.regional.saudi_arabia.setup import add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if company:
add_print_formats()
return
if frappe.db.exists('DocType', 'Print Format'):
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in ('KSA VAT Invoice', 'KSA POS Invoice'):
frappe.db.set_value("Print Format", d, "disabled", 1)

View File

@@ -0,0 +1,16 @@
import frappe
from frappe import _
def execute():
frappe.reload_doctype("Selling Settings")
selling_settings = frappe.get_single("Selling Settings")
if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
selling_settings.customer_group = None
if selling_settings.territory in (_("All Territories"), "All Territories"):
selling_settings.territory = None
selling_settings.flags.ignore_mandatory=True
selling_settings.save(ignore_permissions=True)

View File

@@ -0,0 +1,32 @@
# Copyright (c) 2020, Wahni Green Technologies and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.utils.rename_field import rename_field
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if not company:
return
if frappe.db.exists('DocType', 'Sales Invoice'):
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
# rename_field method assumes that the field already exists or the doc is synced
if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'):
create_custom_fields({
'Sales Invoice': [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
]
})
if frappe.db.has_column('Sales Invoice', 'qr_code'):
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")

View File

@@ -1086,7 +1086,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
$.each(this.frm.doc.taxes || [], function(i, d) { $.each(this.frm.doc.taxes || [], function(i, d) {
if(d.charge_type == "Actual") { if(d.charge_type == "Actual") {
frappe.model.set_value(d.doctype, d.name, "tax_amount", frappe.model.set_value(d.doctype, d.name, "tax_amount",
flt(d.tax_amount) / flt(exchange_rate)); flt(d.base_tax_amount) / flt(exchange_rate));
} }
}); });
}, },

View File

@@ -84,6 +84,10 @@ $.extend(erpnext, {
}); });
}, },
route_to_pending_reposts: (args) => {
frappe.set_route('List', 'Repost Item Valuation', args);
},
proceed_save_with_reminders_frequency_change: () => { proceed_save_with_reminders_frequency_change: () => {
frappe.ui.hide_open_dialog(); frappe.ui.hide_open_dialog();

View File

@@ -495,6 +495,11 @@
font-size: var(--text-md); font-size: var(--text-md);
} }
> .item-qty-total-container {
@extend .net-total-container;
padding: 5px 0px 0px 0px;
}
> .taxes-container { > .taxes-container {
display: none; display: none;
flex-direction: column; flex-direction: column;

View File

@@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document):
memberships = frappe.db.get_all('Membership', { memberships = frappe.db.get_all('Membership', {
'member': self.member, 'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled') 'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')

View File

@@ -561,17 +561,17 @@ def get_item_list(data, doc, hsn_wise=False):
} }
item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol']
hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise)
for hsn_code, taxable_amount in hsn_taxable_amount.items(): for item_or_hsn, taxable_amount in hsn_taxable_amount.items():
item_data = frappe._dict() item_data = frappe._dict()
if not hsn_code: if not item_or_hsn:
frappe.throw(_('GST HSN Code does not exist for one or more items')) frappe.throw(_('GST HSN Code does not exist for one or more items'))
item_data.hsnCode = int(hsn_code) item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn
item_data.taxableAmount = taxable_amount item_data.taxableAmount = taxable_amount
item_data.qtyUnit = "" item_data.qtyUnit = ""
for attr in item_data_attrs: for attr in item_data_attrs:
item_data[attr] = 0 item_data[attr] = 0
for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items(): for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items():
account_type = gst_accounts.get(account, '') account_type = gst_accounts.get(account, '')
for tax_acc, attrs in tax_map.items(): for tax_acc, attrs in tax_map.items():
if account_type == tax_acc: if account_type == tax_acc:
@@ -839,13 +839,11 @@ def update_taxable_values(doc, method):
doc.get('items')[item_count - 1].taxable_value += diff doc.get('items')[item_count - 1].taxable_value += diff
def get_depreciation_amount(asset, depreciable_value, row): def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life: if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(row.value_after_depreciation) - depreciation_amount = (flt(asset.gross_purchase_amount) -
flt(row.expected_value_after_useful_life)) / depreciation_left flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair # if the Depreciation Schedule is being modified after Asset Repair
else: else:

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,32 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-12-07 13:25:05.424827",
"css": "",
"custom_format": 1,
"default_print_language": "en",
"disabled": 1,
"doc_type": "POS Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 0,
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2021-12-08 10:25:01.930885",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA POS Invoice",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

File diff suppressed because one or more lines are too long

View File

@@ -115,9 +115,11 @@ def get_items(filters):
items = frappe.db.sql(""" items = frappe.db.sql("""
select select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, `tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount, sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description `tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
@@ -125,6 +127,8 @@ def get_items(filters):
and `tabSales Invoice`.docstatus = 1 and `tabSales Invoice`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
group by
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
""" % (conditions, match_conditions), filters, as_dict=1) """ % (conditions, match_conditions), filters, as_dict=1)

View File

@@ -0,0 +1,89 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from unittest import TestCase
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
make_company as setup_company,
)
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
make_customers as setup_customers,
)
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
set_account_heads as setup_gst_settings,
)
from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import (
execute as run_report,
)
from erpnext.stock.doctype.item.test_item import make_item
class TestHSNWiseSummaryReport(TestCase):
@classmethod
def setUpClass(cls):
setup_company()
setup_customers()
setup_gst_settings()
make_item("Golf Car", properties={ "gst_hsn_code": "999900" })
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def test_hsn_summary_for_invoice_with_duplicate_items(self):
si = create_sales_invoice(
company="_Test Company GST",
customer = "_Test GST Customer",
currency = "INR",
warehouse = "Finished Goods - _GST",
debit_to = "Debtors - _GST",
income_account = "Sales - _GST",
expense_account = "Cost of Goods Sold - _GST",
cost_center = "Main - _GST",
do_not_save=1
)
si.items = []
si.append("items", {
"item_code": "Golf Car",
"gst_hsn_code": "999900",
"qty": "1",
"rate": "120",
"cost_center": "Main - _GST"
})
si.append("items", {
"item_code": "Golf Car",
"gst_hsn_code": "999900",
"qty": "1",
"rate": "140",
"cost_center": "Main - _GST"
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
})
si.posting_date = "2020-11-17"
si.submit()
si.reload()
[columns, data] = run_report(filters=frappe._dict({
"company": "_Test Company GST",
"gst_hsn_code": "999900",
"company_gstin": si.company_gstin,
"from_date": si.posting_date,
"to_date": si.posting_date
}))
filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data))
self.assertTrue(filtered_rows)
hsn_row = filtered_rows[0]
self.assertEquals(hsn_row['stock_qty'], 2.0)
self.assertEquals(hsn_row['total_amount'], 306.8)

View File

@@ -3,7 +3,7 @@
import frappe import frappe
from frappe.permissions import add_permission, update_permission_property from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@@ -13,6 +13,16 @@ def setup(company=None, patch=True):
add_permissions() add_permissions()
make_custom_fields() make_custom_fields()
def add_print_formats():
frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'):
frappe.db.set_value("Print Format", d, "disabled", 0)
def add_permissions(): def add_permissions():
"""Add Permissions for KSA VAT Setting.""" """Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0) add_permission('KSA VAT Setting', 'All', 0)
@@ -33,8 +43,16 @@ def make_custom_fields():
custom_fields = { custom_fields = {
'Sales Invoice': [ 'Sales Invoice': [
dict( dict(
fieldname='qr_code', fieldname='ksa_einv_qr',
label='QR Code', label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
],
'POS Invoice': [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image', fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1 read_only=1, no_copy=1, hidden=1
) )

View File

@@ -1,147 +1,152 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import io import io
import os import os
from base64 import b64encode from base64 import b64encode
import frappe import frappe
from frappe import _ from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils.data import add_to_date, get_time, getdate from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create from pyqrcode import create as qr_create
from erpnext import get_region from erpnext import get_region
def create_qr_code(doc, method): def create_qr_code(doc, method=None):
"""Create QR Code after inserting Sales Inv
"""
region = get_region(doc.company) region = get_region(doc.company)
if region not in ['Saudi Arabia']: if region not in ['Saudi Arabia']:
return return
# if QR Code field not present, do nothing # if QR Code field not present, create it. Invoices without QR are invalid as per law.
if not hasattr(doc, 'qr_code'): if not hasattr(doc, 'ksa_einv_qr'):
return create_custom_fields({
doc.doctype: [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
]
})
# Don't create QR Code if it already exists # Don't create QR Code if it already exists
qr_code = doc.get("qr_code") qr_code = doc.get("ksa_einv_qr")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}): if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return return
meta = frappe.get_meta('Sales Invoice') meta = frappe.get_meta(doc.doctype)
for field in meta.get_image_fields(): if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
if field.fieldname == 'qr_code': ''' TLV conversion for
''' TLV conversion for 1. Seller's Name
1. Seller's Name 2. VAT Number
2. VAT Number 3. Time Stamp
3. Time Stamp 4. Invoice Amount
4. Invoice Amount 5. VAT Amount
5. VAT Amount '''
''' tlv_array = []
tlv_array = [] # Sellers Name
# Sellers Name
seller_name = frappe.db.get_value( seller_name = frappe.db.get_value(
'Company', 'Company',
doc.company, doc.company,
'company_name_in_arabic') 'company_name_in_arabic')
if not seller_name: if not seller_name:
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
tag = bytes([1]).hex() tag = bytes([1]).hex()
length = bytes([len(seller_name.encode('utf-8'))]).hex() length = bytes([len(seller_name.encode('utf-8'))]).hex()
value = seller_name.encode('utf-8').hex() value = seller_name.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# VAT Number # VAT Number
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
if not tax_id: if not tax_id:
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
tag = bytes([2]).hex() tag = bytes([2]).hex()
length = bytes([len(tax_id)]).hex() length = bytes([len(tax_id)]).hex()
value = tax_id.encode('utf-8').hex() value = tax_id.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Time Stamp # Time Stamp
posting_date = getdate(doc.posting_date) posting_date = getdate(doc.posting_date)
time = get_time(doc.posting_time) time = get_time(doc.posting_time)
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
time_stamp = add_to_date(posting_date, seconds=seconds) time_stamp = add_to_date(posting_date, seconds=seconds)
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
tag = bytes([3]).hex() tag = bytes([3]).hex()
length = bytes([len(time_stamp)]).hex() length = bytes([len(time_stamp)]).hex()
value = time_stamp.encode('utf-8').hex() value = time_stamp.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Invoice Amount # Invoice Amount
invoice_amount = str(doc.grand_total) invoice_amount = str(doc.grand_total)
tag = bytes([4]).hex() tag = bytes([4]).hex()
length = bytes([len(invoice_amount)]).hex() length = bytes([len(invoice_amount)]).hex()
value = invoice_amount.encode('utf-8').hex() value = invoice_amount.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# VAT Amount # VAT Amount
vat_amount = str(doc.total_taxes_and_charges) vat_amount = str(doc.total_taxes_and_charges)
tag = bytes([5]).hex() tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).hex() length = bytes([len(vat_amount)]).hex()
value = vat_amount.encode('utf-8').hex() value = vat_amount.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Joining bytes into one # Joining bytes into one
tlv_buff = ''.join(tlv_array) tlv_buff = ''.join(tlv_array)
# base64 conversion for QR Code # base64 conversion for QR Code
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
qr_image = io.BytesIO() qr_image = io.BytesIO()
url = qr_create(base64_string, error='L') url = qr_create(base64_string, error='L')
url.png(qr_image, scale=2, quiet_zone=1) url.png(qr_image, scale=2, quiet_zone=1)
name = frappe.generate_hash(doc.name, 5) name = frappe.generate_hash(doc.name, 5)
# making file # making file
filename = f"QRCode-{name}.png".replace(os.path.sep, "__") filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc({ _file = frappe.get_doc({
"doctype": "File", "doctype": "File",
"file_name": filename, "file_name": filename,
"is_private": 0, "is_private": 0,
"content": qr_image.getvalue(), "content": qr_image.getvalue(),
"attached_to_doctype": doc.get("doctype"), "attached_to_doctype": doc.get("doctype"),
"attached_to_name": doc.get("name"), "attached_to_name": doc.get("name"),
"attached_to_field": "qr_code" "attached_to_field": "ksa_einv_qr"
}) })
_file.save() _file.save()
# assigning to document # assigning to document
doc.db_set('qr_code', _file.file_url) doc.db_set('ksa_einv_qr', _file.file_url)
doc.notify_update() doc.notify_update()
break
def delete_qr_code_file(doc, method): def delete_qr_code_file(doc, method=None):
"""Delete QR Code on deleted sales invoice"""
region = get_region(doc.company) region = get_region(doc.company)
if region not in ['Saudi Arabia']: if region not in ['Saudi Arabia']:
return return
if hasattr(doc, 'qr_code'): if hasattr(doc, 'ksa_einv_qr'):
if doc.get('qr_code'): if doc.get('ksa_einv_qr'):
file_doc = frappe.get_list('File', { file_doc = frappe.get_list('File', {
'file_url': doc.get('qr_code') 'file_url': doc.get('ksa_einv_qr')
}) })
if len(file_doc): if len(file_doc):
frappe.delete_doc('File', file_doc[0].name) frappe.delete_doc('File', file_doc[0].name)
def delete_vat_settings_for_company(doc, method): def delete_vat_settings_for_company(doc, method=None):
if doc.country != 'Saudi Arabia': if doc.country != 'Saudi Arabia':
return return
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name}) if frappe.db.exists('KSA VAT Setting', doc.name):
settings_doc.delete() frappe.delete_doc('KSA VAT Setting', doc.name)

View File

@@ -952,8 +952,7 @@
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"max_attachments": 1, "modified": "2021-11-30 01:33:21.106073",
"modified": "2021-08-27 20:10:07.864951",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@@ -134,6 +134,7 @@
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break7", "column_break7",
"amount_eligible_for_commission",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break1", "section_break1",
@@ -1507,16 +1508,23 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Dispatch Address", "label": "Dispatch Address",
"read_only": 1 "read_only": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-09-28 13:09:51.515542", "modified": "2021-10-05 12:16:40.775704",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -64,6 +64,8 @@ class SalesOrder(SellingController):
if not self.billing_status: self.billing_status = 'Not Billed' if not self.billing_status: self.billing_status = 'Not Billed'
if not self.delivery_status: self.delivery_status = 'Not Delivered' if not self.delivery_status: self.delivery_status = 'Not Delivered'
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_po(self): def validate_po(self):
# validate p.o date v/s delivery date # validate p.o date v/s delivery date
if self.po_date and not self.skip_delivery_note: if self.po_date and not self.skip_delivery_note:
@@ -1024,6 +1026,7 @@ def make_work_orders(items, sales_order, company, project=None):
description=i['description'] description=i['description']
)).insert() )).insert()
work_order.set_work_order_operations() work_order.set_work_order_operations()
work_order.flags.ignore_mandatory = True
work_order.save() work_order.save()
out.append(work_order) out.append(work_order)

View File

@@ -48,6 +48,7 @@
"pricing_rules", "pricing_rules",
"stock_uom_rate", "stock_uom_rate",
"is_free_item", "is_free_item",
"grant_commission",
"section_break_24", "section_break_24",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -789,15 +790,23 @@
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-23 01:15:05.803091", "modified": "2021-10-05 12:27:25.014789",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -8,7 +8,6 @@ import frappe
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.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.nestedset import get_root_of
class SellingSettings(Document): class SellingSettings(Document):
@@ -37,9 +36,3 @@ class SellingSettings(Document):
editable_bundle_item_rates = cint(self.editable_bundle_item_rates) editable_bundle_item_rates = cint(self.editable_bundle_item_rates)
make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False)
def set_default_customer_group_and_territory(self):
if not self.customer_group:
self.customer_group = get_root_of('Customer Group')
if not self.territory:
self.territory = get_root_of('Territory')

View File

@@ -100,6 +100,10 @@ erpnext.PointOfSale.ItemCart = class {
`<div class="add-discount-wrapper"> `<div class="add-discount-wrapper">
${this.get_discount_icon()} ${__('Add Discount')} ${this.get_discount_icon()} ${__('Add Discount')}
</div> </div>
<div class="item-qty-total-container">
<div class="item-qty-total-label">${__('Total Items')}</div>
<div class="item-qty-total-value">0.00</div>
</div>
<div class="net-total-container"> <div class="net-total-container">
<div class="net-total-label">${__("Net Total")}</div> <div class="net-total-label">${__("Net Total")}</div>
<div class="net-total-value">0.00</div> <div class="net-total-value">0.00</div>
@@ -142,6 +146,7 @@ erpnext.PointOfSale.ItemCart = class {
this.$numpad_section.prepend( this.$numpad_section.prepend(
`<div class="numpad-totals"> `<div class="numpad-totals">
<span class="numpad-item-qty-total"></span>
<span class="numpad-net-total"></span> <span class="numpad-net-total"></span>
<span class="numpad-grand-total"></span> <span class="numpad-grand-total"></span>
</div>` </div>`
@@ -470,6 +475,7 @@ erpnext.PointOfSale.ItemCart = class {
if (!frm) frm = this.events.get_frm(); if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total); this.render_net_total(frm.doc.net_total);
this.render_total_item_qty(frm.doc.items);
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total); this.render_grand_total(grand_total);
@@ -487,6 +493,21 @@ erpnext.PointOfSale.ItemCart = class {
); );
} }
render_total_item_qty(items) {
var total_item_qty = 0;
items.map((item) => {
total_item_qty = total_item_qty + item.qty;
});
this.$totals_section.find('.item-qty-total-container').html(
`<div>${__('Total Quantity')}</div><div>${total_item_qty}</div>`
);
this.$numpad_section.find('.numpad-item-qty-total').html(
`<div>${__('Total Quantity')}: <span>${total_item_qty}</span></div>`
);
}
render_grand_total(value) { render_grand_total(value) {
const currency = this.events.get_frm().doc.currency; const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.grand-total-container').html( this.$totals_section.find('.grand-total-container').html(

View File

@@ -157,25 +157,19 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
commission_rate: function() { commission_rate: function() {
this.calculate_commission(); this.calculate_commission();
refresh_field("total_commission");
}, },
total_commission: function() { total_commission: function() {
if(this.frm.doc.base_net_total) { frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]);
if(this.frm.doc.base_net_total < this.frm.doc.total_commission) { const { amount_eligible_for_commission } = this.frm.doc;
var msg = (__("[Error]") + " " + if (!amount_eligible_for_commission) return;
__(frappe.meta.get_label(this.frm.doc.doctype, "total_commission",
this.frm.doc.name)) + " > " +
__(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name)));
frappe.msgprint(msg);
throw msg;
}
this.frm.set_value("commission_rate", this.frm.set_value(
flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total)); "commission_rate", flt(
} this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
)
);
}, },
allocated_percentage: function(doc, cdt, cdn) { allocated_percentage: function(doc, cdt, cdn) {
@@ -185,7 +179,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
sales_person.allocated_percentage = flt(sales_person.allocated_percentage, sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
precision("allocated_percentage", sales_person)); precision("allocated_percentage", sales_person));
sales_person.allocated_amount = flt(this.frm.doc.base_net_total * sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
sales_person.allocated_percentage / 100.0, sales_person.allocated_percentage / 100.0,
precision("allocated_amount", sales_person)); precision("allocated_amount", sales_person));
refresh_field(["allocated_amount"], sales_person); refresh_field(["allocated_amount"], sales_person);
@@ -259,28 +253,39 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
}, },
calculate_commission: function() { calculate_commission: function() {
if(this.frm.fields_dict.commission_rate) { if (!this.frm.fields_dict.commission_rate) return;
if(this.frm.doc.commission_rate > 100) {
var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) +
" " + __("cannot be greater than 100");
frappe.msgprint(msg);
throw msg;
}
this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0, if (this.frm.doc.commission_rate > 100) {
precision("total_commission")); this.frm.set_value("commission_rate", 100);
frappe.throw(`${__(frappe.meta.get_label(
this.frm.doc.doctype, "commission_rate", this.frm.doc.name
))} ${__("cannot be greater than 100")}`);
} }
this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
(sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
)
this.frm.doc.total_commission = flt(
this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
precision("total_commission")
);
refresh_field(["amount_eligible_for_commission", "total_commission"]);
}, },
calculate_contribution: function() { calculate_contribution: function() {
var me = this; var me = this;
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
frappe.model.round_floats_in(sales_person); frappe.model.round_floats_in(sales_person);
if(sales_person.allocated_percentage) { if (!sales_person.allocated_percentage) return;
sales_person.allocated_amount = flt(
me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0, sales_person.allocated_amount = flt(
precision("allocated_amount", sales_person)); me.frm.doc.amount_eligible_for_commission
} * sales_person.allocated_percentage
/ 100.0,
precision("allocated_amount", sales_person)
);
}); });
}, },

View File

@@ -304,7 +304,6 @@ def set_more_defaults():
def update_selling_defaults(): def update_selling_defaults():
selling_settings = frappe.get_doc("Selling Settings") selling_settings = frappe.get_doc("Selling Settings")
selling_settings.set_default_customer_group_and_territory()
selling_settings.cust_master_name = "Customer Name" selling_settings.cust_master_name = "Customer Name"
selling_settings.so_required = "No" selling_settings.so_required = "No"
selling_settings.dn_required = "No" selling_settings.dn_required = "No"

View File

@@ -53,6 +53,7 @@ def before_tests():
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
enable_all_roles_and_domains() enable_all_roles_and_domains()
set_defaults_for_tests()
frappe.db.commit() frappe.db.commit()
@@ -127,6 +128,14 @@ def enable_all_roles_and_domains():
[d.name for d in domains]) [d.name for d in domains])
add_all_roles_to('Administrator') add_all_roles_to('Administrator')
def set_defaults_for_tests():
from frappe.utils.nestedset import get_root_of
selling_settings = frappe.get_single("Selling Settings")
selling_settings.customer_group = get_root_of("Customer Group")
selling_settings.territory = get_root_of("Territory")
selling_settings.save()
def insert_record(records): def insert_record(records):
for r in records: for r in records:

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import flt
class Bin(Document): class Bin(Document):
@@ -100,33 +100,11 @@ def on_doctype_update():
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin''' """WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.stock_ledger import repost_current_voucher
update_qty(bin_name, args) update_qty(bin_name, args)
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": args.get('item_code'),
"warehouse": args.get('warehouse'),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get('name'),
"creation": args.get('creation')
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# update qty in future sle and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def get_bin_details(bin_name): def get_bin_details(bin_name):
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',

View File

@@ -145,6 +145,7 @@
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break7", "column_break7",
"amount_eligible_for_commission",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break1", "section_break1",
@@ -1302,16 +1303,23 @@
"label": "Dispatch Address", "label": "Dispatch Address",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-10-08 14:29:13.428984", "modified": "2021-10-09 14:29:13.428984",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -138,6 +138,7 @@ class DeliveryNote(SellingController):
self.update_current_stock() self.update_current_stock()
if not self.installation_status: self.installation_status = 'Not Installed' if not self.installation_status: self.installation_status = 'Not Installed'
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self): def validate_with_previous_doc(self):
super(DeliveryNote, self).validate_with_previous_doc({ super(DeliveryNote, self).validate_with_previous_doc({

View File

@@ -49,6 +49,7 @@
"pricing_rules", "pricing_rules",
"stock_uom_rate", "stock_uom_rate",
"is_free_item", "is_free_item",
"grant_commission",
"section_break_25", "section_break_25",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -753,13 +754,20 @@
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-10-05 12:12:44.018872", "modified": "2021-10-06 12:12:44.018872",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@@ -89,6 +89,7 @@
"sales_details", "sales_details",
"sales_uom", "sales_uom",
"is_sales_item", "is_sales_item",
"grant_commission",
"column_break3", "column_break3",
"max_discount", "max_discount",
"deferred_revenue", "deferred_revenue",
@@ -942,6 +943,12 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Published in Website", "label": "Published in Website",
"read_only": 1 "read_only": 1
},
{
"default": "1",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission"
} }
], ],
"icon": "fa fa-tag", "icon": "fa fa-tag",
@@ -949,8 +956,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 1, "modified": "2021-12-03 08:32:03.869294",
"modified": "2021-09-10 12:23:07.277077",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",
@@ -1017,7 +1023,7 @@
"search_fields": "item_name,description,item_group,customer_code", "search_fields": "item_name,description,item_group,customer_code",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"show_preview_popup": 1, "show_preview_popup": 1,
"sort_field": "idx desc,modified desc", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "item_name", "title_field": "item_name",
"track_changes": 1 "track_changes": 1

View File

@@ -496,7 +496,6 @@ class Item(Document):
def recalculate_bin_qty(self, new_name): def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock from erpnext.stock.stock_balance import repost_stock
frappe.db.auto_commit_on_many_writes = 1
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -510,7 +509,6 @@ class Item(Document):
repost_stock(new_name, warehouse) repost_stock(new_name, warehouse)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
def update_bom_item_desc(self): def update_bom_item_desc(self):
if self.is_new(): if self.is_new():

View File

@@ -81,6 +81,9 @@ class MaterialRequest(BuyingController):
# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated # NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
# Though the creation of Material Request from a Production Plan can be rethought to fix this # Though the creation of Material Request from a Production Plan can be rethought to fix this
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def set_title(self): def set_title(self):
'''Set title as comma separated list of items''' '''Set title as comma separated list of items'''
if not self.title: if not self.title:

View File

@@ -119,6 +119,10 @@ class PurchaseReceipt(BuyingController):
if getdate(self.posting_date) > getdate(nowdate()): if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date")) throw(_("Posting Date cannot be future date"))
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_cwip_accounts(self): def validate_cwip_accounts(self):
for item in self.get('items'): for item in self.get('items'):

View File

@@ -168,8 +168,8 @@ def repost_entries():
for row in riv_entries: for row in riv_entries:
doc = frappe.get_doc('Repost Item Valuation', row.name) doc = frappe.get_doc('Repost Item Valuation', row.name)
if doc.status in ('Queued', 'In Progress'): if doc.status in ('Queued', 'In Progress'):
doc.deduplicate_similar_repost()
repost(doc) repost(doc)
doc.deduplicate_similar_repost()
riv_entries = get_repost_item_valuation_entries() riv_entries = get_repost_item_valuation_entries()
if riv_entries: if riv_entries:

View File

@@ -4,12 +4,14 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import nowdate
from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.controllers.stock_controller import create_item_wise_repost_entries
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
in_configured_timeslot, in_configured_timeslot,
) )
from erpnext.stock.utils import PendingRepostingError
class TestRepostItemValuation(unittest.TestCase): class TestRepostItemValuation(unittest.TestCase):
@@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase):
# to avoid breaking other tests accidentaly # to avoid breaking other tests accidentaly
riv4.set_status("Skipped") riv4.set_status("Skipped")
riv3.set_status("Skipped") riv3.set_status("Skipped")
def test_stock_freeze_validation(self):
today = nowdate()
riv = frappe.get_doc(
doctype="Repost Item Valuation",
item_code="_Test Item",
warehouse="_Test Warehouse - _TC",
based_on="Item and Warehouse",
posting_date=today,
posting_time="00:01:00",
)
riv.flags.dont_run_in_test = True # keep it queued
riv.submit()
stock_settings = frappe.get_doc("Stock Settings")
stock_settings.stock_frozen_upto = today
self.assertRaises(PendingRepostingError, stock_settings.save)
riv.set_status("Skipped")

View File

@@ -104,6 +104,8 @@ class StockEntry(StockController):
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount() self.calculate_rate_and_amount()
self.validate_putaway_capacity() self.validate_putaway_capacity()
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def on_submit(self): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()

View File

@@ -25,7 +25,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
from erpnext.tests.utils import ERPNextTestCase, change_settings
def get_sle(**args): def get_sle(**args):
@@ -39,7 +40,7 @@ def get_sle(**args):
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
values, as_dict=1) values, as_dict=1)
class TestStockEntry(unittest.TestCase): class TestStockEntry(ERPNextTestCase):
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@@ -929,6 +930,83 @@ class TestStockEntry(unittest.TestCase):
distributed_costs = [d.additional_cost for d in se.items] distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs) self.assertEqual([40.0, 60.0], distributed_costs)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty
item_code = '_Test Future Neg Item'
batch_no = '_Test Future Neg Batch'
warehouses = [
'_Test Future Neg Warehouse Source',
'_Test Future Neg Warehouse Destination'
]
warehouse_names = initialize_records_for_future_negative_sle_test(
item_code, batch_no, warehouses,
opening_qty=2, posting_date='2021-07-01'
)
# Executing an illegal sequence should raise an error
sequence_of_entries = [
dict(item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date='2021-07-03',
purpose='Material Transfer'),
dict(item_code=item_code,
qty=2,
from_warehouse=warehouse_names[1],
to_warehouse=warehouse_names[0],
batch_no=batch_no,
posting_date='2021-07-04',
purpose='Material Transfer'),
dict(item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date='2021-07-02', # Illegal SE
purpose='Material Transfer')
]
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle_batch(self):
from erpnext.stock.doctype.batch.test_batch import TestBatch
# Initialize item, batch, warehouse, opening qty
item_code = '_Test MultiBatch Item'
TestBatch.make_batch_item(item_code)
batch_nos = [] # store generate batches
warehouse = '_Test Warehouse - _TC'
se1 = make_stock_entry(
item_code=item_code,
qty=2,
to_warehouse=warehouse,
posting_date='2021-09-01',
purpose='Material Receipt'
)
batch_nos.append(se1.items[0].batch_no)
se2 = make_stock_entry(
item_code=item_code,
qty=2,
to_warehouse=warehouse,
posting_date='2021-09-03',
purpose='Material Receipt'
)
batch_nos.append(se2.items[0].batch_no)
with self.assertRaises(NegativeStockError) as nse:
make_stock_entry(item_code=item_code,
qty=1,
from_warehouse=warehouse,
batch_no=batch_nos[1],
posting_date='2021-09-02', # backdated consumption of 2nd batch
purpose='Material Issue')
def make_serialized_item(**args): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)
se = frappe.copy_doc(test_records[0]) se = frappe.copy_doc(test_records[0])
@@ -999,3 +1077,31 @@ def get_multiple_items():
] ]
test_records = frappe.get_test_records('Stock Entry') test_records = frappe.get_test_records('Stock Entry')
def initialize_records_for_future_negative_sle_test(
item_code, batch_no, warehouses, opening_qty, posting_date):
from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
TestBatch.make_batch_item(item_code)
make_new_batch(item_code=item_code, batch_id=batch_no)
warehouse_names = [create_warehouse(w) for w in warehouses]
create_stock_reconciliation(
purpose='Opening Stock',
posting_date=posting_date,
posting_time='20:00:20',
item_code=item_code,
warehouse=warehouse_names[0],
valuation_rate=100,
qty=opening_qty,
batch_no=batch_no,
)
return warehouse_names
def create_stock_entries(sequence_of_entries):
for entry_detail in sequence_of_entries:
make_stock_entry(**entry_detail)

View File

@@ -9,7 +9,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.core.doctype.role.role import get_users from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -44,7 +44,6 @@ class StockLedgerEntry(Document):
def on_submit(self): def on_submit(self):
self.check_stock_frozen_date() self.check_stock_frozen_date()
self.actual_amt_check()
self.calculate_batch_qty() self.calculate_batch_qty()
if not self.get("via_landed_cost_voucher"): if not self.get("via_landed_cost_voucher"):
@@ -58,18 +57,6 @@ class StockLedgerEntry(Document):
"sum(actual_qty)") or 0 "sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
def actual_amt_check(self):
"""Validate that qty at warehouse for selected batch is >=0"""
if self.batch_no and not self.get("allow_negative_stock"):
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry`
where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
(self.warehouse, self.item_code, self.batch_no))[0][0])
if batch_bal_after_transaction < 0:
frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}")
.format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse))
def validate_mandatory(self): def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory: for k in mandatory:

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-03-28 10:35:31", "creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
@@ -153,11 +154,12 @@
"icon": "fa fa-upload-alt", "icon": "fa fa-upload-alt",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"max_attachments": 1, "links": [],
"modified": "2020-04-08 17:02:47.196206", "modified": "2021-11-30 01:33:51.437194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reconciliation", "name": "Stock Reconciliation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -11,6 +11,8 @@ from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.html_utils import clean_html from frappe.utils.html_utils import clean_html
from erpnext.stock.utils import check_pending_reposting
class StockSettings(Document): class StockSettings(Document):
def validate(self): def validate(self):
@@ -36,6 +38,7 @@ class StockSettings(Document):
self.validate_warehouses() self.validate_warehouses()
self.cant_change_valuation_method() self.cant_change_valuation_method()
self.validate_clean_description_html() self.validate_clean_description_html()
self.validate_pending_reposts()
def validate_warehouses(self): def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -64,6 +67,11 @@ class StockSettings(Document):
# changed to text # changed to text
frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test)
def validate_pending_reposts(self):
if self.stock_frozen_upto:
check_pending_reposting(self.stock_frozen_upto)
def on_update(self): def on_update(self):
self.toggle_warehouse_field_for_inter_warehouse_transfer() self.toggle_warehouse_field_for_inter_warehouse_transfer()

View File

@@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase):
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0) self.assertEqual(child_warehouse.is_group, 0)
def test_warehouse_renaming(self):
create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
# Rename with abbr
if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Rename without abbr
if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Another rename with multiple dashes
if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
def test_warehouse_merging(self):
company = "_Test Company with perpetual inventory"
create_warehouse("Test Warehouse for Merging 1", company=company,
properties={"parent_warehouse": "All Warehouses - TCP1"})
create_warehouse("Test Warehouse for Merging 2", company=company,
properties={"parent_warehouse": "All Warehouses - TCP1"})
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
qty=1, rate=100, company=company)
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
qty=1, rate=100, company=company)
existing_bin_qty = (
cint(frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
+ cint(frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
)
frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
"Test Warehouse for Merging 2 - TCP1", merge=True)
self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
bin_qty = frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
self.assertEqual(bin_qty, existing_bin_qty)
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def test_unlinking_warehouse_from_item_defaults(self): def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company" company = "_Test Company"

View File

@@ -1,7 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1,
"creation": "2013-03-07 18:50:32", "creation": "2013-03-07 18:50:32",
"description": "A logical Warehouse against which stock entries are made.", "description": "A logical Warehouse against which stock entries are made.",
"doctype": "DocType", "doctype": "DocType",
@@ -245,7 +244,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-04-09 19:54:56.263965", "modified": "2021-12-03 04:40:06.414630",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Warehouse", "name": "Warehouse",

View File

@@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.utils import cint, flt from frappe.utils import cint, flt
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
import erpnext
from erpnext.stock import get_warehouse_account from erpnext.stock import get_warehouse_account
@@ -68,57 +67,6 @@ class Warehouse(NestedSet):
return frappe.db.sql("""select name from `tabWarehouse` return frappe.db.sql("""select name from `tabWarehouse`
where parent_warehouse = %s limit 1""", self.name) where parent_warehouse = %s limit 1""", self.name)
def before_rename(self, old_name, new_name, merge=False):
super(Warehouse, self).before_rename(old_name, new_name, merge)
# Add company abbr if not provided
new_warehouse = erpnext.encode_company_abbr(new_name, self.company)
if merge:
if not frappe.db.exists("Warehouse", new_warehouse):
frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse))
if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"):
frappe.throw(_("Both Warehouse must belong to same Company"))
return new_warehouse
def after_rename(self, old_name, new_name, merge=False):
super(Warehouse, self).after_rename(old_name, new_name, merge)
new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name)
self.db_set("warehouse_name", new_warehouse_name)
if merge:
self.recalculate_bin_qty(new_name)
def get_new_warehouse_name_without_abbr(self, name):
company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() == company_abbr.lower():
name = parts[0]
return name
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
frappe.db.auto_commit_on_many_writes = 1
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
repost_stock_for_items = frappe.db.sql_list("""select distinct item_code
from tabBin where warehouse=%s""", new_name)
# Delete all existing bins to avoid duplicate bins for the same item and warehouse
frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name)
for item_code in repost_stock_for_items:
repost_stock(item_code, new_name)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
def convert_to_group_or_ledger(self): def convert_to_group_or_ledger(self):
if self.is_group: if self.is_group:
self.convert_to_ledger() self.convert_to_ledger()

View File

@@ -328,7 +328,8 @@ def get_basic_details(args, item, overwrite_warehouse=True):
"against_blanket_order": args.get("against_blanket_order"), "against_blanket_order": args.get("against_blanket_order"),
"bom_no": item.get("default_bom"), "bom_no": item.get("default_bom"),
"weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
"weight_uom": args.get("weight_uom") or item.get("weight_uom") "weight_uom": args.get("weight_uom") or item.get("weight_uom"),
"grant_commission": item.get("grant_commission")
}) })
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
const DIFFERNCE_FIELD_NAMES = [
"difference_in_qty",
"fifo_qty_diff",
"fifo_value_diff",
"fifo_valuation_diff",
"valuation_diff",
"fifo_difference_diff"
];
frappe.query_reports["Stock Ledger Invariant Check"] = {
"filters": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item",
"mandatory": 1,
"options": "Item",
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
}
}
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"mandatory": 1,
"options": "Warehouse",
}
],
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;
},
};

View File

@@ -0,0 +1,26 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-12-16 06:31:23.290916",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-12-16 09:55:58.341764",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Invariant Check",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "Stock Ledger Invariant Check",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
}
]
}

View File

@@ -0,0 +1,236 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# License: GNU GPL v3. See LICENSE
import json
import frappe
SLE_FIELDS = (
"name",
"posting_date",
"posting_time",
"creation",
"voucher_type",
"voucher_no",
"actual_qty",
"qty_after_transaction",
"incoming_rate",
"outgoing_rate",
"stock_queue",
"batch_no",
"stock_value",
"stock_value_difference",
"valuation_rate",
)
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters):
sles = get_stock_ledger_entries(filters)
return add_invariant_check_fields(sles)
def get_stock_ledger_entries(filters):
return frappe.get_all(
"Stock Ledger Entry",
fields=SLE_FIELDS,
filters={
"item_code": filters.item_code,
"warehouse": filters.warehouse,
"is_cancelled": 0
},
order_by="timestamp(posting_date, posting_time), creation",
)
def add_invariant_check_fields(sles):
balance_qty = 0.0
for idx, sle in enumerate(sles):
queue = json.loads(sle.stock_queue)
fifo_qty = 0.0
fifo_value = 0.0
for qty, rate in queue:
fifo_qty += qty
fifo_value += qty * rate
balance_qty += sle.actual_qty
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
sle.balance_value_by_qty = (
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
)
sle.expected_qty_after_transaction = balance_qty
# set difference fields
sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
sle.fifo_value_diff = sle.stock_value - fifo_value
sle.fifo_valuation_diff = (
sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
)
sle.valuation_diff = (
sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
)
if idx > 0:
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
return sles
def get_columns():
return [
{
"fieldname": "name",
"fieldtype": "Link",
"label": "Stock Ledger Entry",
"options": "Stock Ledger Entry",
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
},
{
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
},
{
"fieldname": "creation",
"fieldtype": "Datetime",
"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": "actual_qty",
"fieldtype": "Float",
"label": "Qty Change",
},
{
"fieldname": "incoming_rate",
"fieldtype": "Float",
"label": "Incoming Rate",
},
{
"fieldname": "outgoing_rate",
"fieldtype": "Float",
"label": "Outgoing Rate",
},
{
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"label": "(A) Qty After Transaction",
},
{
"fieldname": "expected_qty_after_transaction",
"fieldtype": "Float",
"label": "(B) Expected Qty After Transaction",
},
{
"fieldname": "difference_in_qty",
"fieldtype": "Float",
"label": "A - B",
},
{
"fieldname": "stock_queue",
"fieldtype": "Data",
"label": "FIFO 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": "stock_value_difference",
"fieldtype": "Float",
"label": "(F) Stock Value Difference",
},
{
"fieldname": "fifo_stock_diff",
"fieldtype": "Float",
"label": "(G) Stock Value difference (FIFO queue)",
},
{
"fieldname": "fifo_difference_diff",
"fieldtype": "Float",
"label": "F - G",
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
"label": "(H) Valuation Rate",
},
{
"fieldname": "fifo_valuation_rate",
"fieldtype": "Float",
"label": "(I) Valuation Rate as per FIFO",
},
{
"fieldname": "fifo_valuation_diff",
"fieldtype": "Float",
"label": "H - I",
},
{
"fieldname": "balance_value_by_qty",
"fieldtype": "Float",
"label": "(J) Valuation = Value (D) ÷ Qty (A)",
},
{
"fieldname": "valuation_diff",
"fieldtype": "Float",
"label": "H - J",
},
]

View File

@@ -41,6 +41,12 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Total Stock Summary", {"group_by": "warehouse",}), ("Total Stock Summary", {"group_by": "warehouse",}),
("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"
}
),
] ]
OPTIONAL_FILTERS = { OPTIONAL_FILTERS = {

View File

@@ -7,10 +7,11 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
from six import iteritems from six import iteritems
import erpnext import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.utils import ( from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel, get_incoming_outgoing_rate_for_cancel,
get_or_make_bin, get_or_make_bin,
@@ -18,19 +19,15 @@ from erpnext.stock.utils import (
) )
# future reposting
class NegativeStockError(frappe.ValidationError): pass class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError): class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass pass
_exceptions = frappe.local('stockledger_exceptions') _exceptions = frappe.local('stockledger_exceptions')
# _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries: if sl_entries:
from erpnext.stock.utils import update_bin
cancel = sl_entries[0].get("is_cancelled") cancel = sl_entries[0].get("is_cancelled")
if cancel: if cancel:
validate_cancellation(sl_entries) validate_cancellation(sl_entries)
@@ -65,7 +62,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
# preserve previous_qty_after_transaction for qty reposting # preserve previous_qty_after_transaction for qty reposting
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
update_bin(args, allow_negative_stock, via_landed_cost_voucher) is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
update_bin_qty(bin_name, args)
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": args.get('item_code'),
"warehouse": args.get('warehouse'),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get('name'),
"creation": args.get('creation')
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# update qty in future sle and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def get_args_for_future_sle(row): def get_args_for_future_sle(row):
return frappe._dict({ return frappe._dict({
@@ -795,10 +823,10 @@ class update_entries_after(object):
def update_bin(self): def update_bin(self):
# update bin for each warehouse # update bin for each warehouse
for warehouse, data in iteritems(self.data): for warehouse, data in self.data.items():
bin_record = get_or_make_bin(self.item_code, warehouse) bin_name = get_or_make_bin(self.item_code, warehouse)
frappe.db.set_value('Bin', bin_record, { frappe.db.set_value('Bin', bin_name, {
"valuation_rate": data.valuation_rate, "valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction, "actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value "stock_value": data.stock_value
@@ -1054,17 +1082,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
allow_negative_stock = cint(allow_negative_stock) \ allow_negative_stock = cint(allow_negative_stock) \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: if allow_negative_stock:
sle = get_future_sle_with_negative_qty(args) return
if sle: if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( return
abs(sle[0]["qty_after_transaction"]),
frappe.get_desk_link('Item', args.item_code), neg_sle = get_future_sle_with_negative_qty(args)
frappe.get_desk_link('Warehouse', args.warehouse), if neg_sle:
sle[0]["posting_date"], sle[0]["posting_time"], message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) abs(neg_sle[0]["qty_after_transaction"]),
frappe.get_desk_link('Item', args.item_code),
frappe.get_desk_link('Warehouse', args.warehouse),
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
if not args.batch_no:
return
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
if neg_batch_sle:
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
abs(neg_batch_sle[0]["cumulative_total"]),
frappe.get_desk_link('Batch', args.batch_no),
frappe.get_desk_link('Warehouse', args.warehouse),
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
def get_future_sle_with_negative_qty(args): def get_future_sle_with_negative_qty(args):
return frappe.db.sql(""" return frappe.db.sql("""
@@ -1083,11 +1130,34 @@ def get_future_sle_with_negative_qty(args):
limit 1 limit 1
""", args, as_dict=1) """, args, as_dict=1)
def get_future_sle_with_negative_batch_qty(args):
return frappe.db.sql("""
with batch_ledger as (
select
posting_date, posting_time, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
from `tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and batch_no=%(batch_no)s
and is_cancelled = 0
order by posting_date, posting_time, creation
)
select * from batch_ledger
where
cumulative_total < 0.0
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
limit 1
""", args, as_dict=1)
def _round_off_if_near_zero(number: float, precision: int = 6) -> float: def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
""" Rounds off the number to zero only if number is close to zero for decimal """ Rounds off the number to zero only if number is close to zero for decimal
specified in precision. Precision defaults to 6. specified in precision. Precision defaults to 6.
""" """
if flt(number) < (1.0 / (10**precision)): if abs(0.0 - flt(number)) < (1.0 / (10**precision)):
return 0 return 0.0
return flt(number) return flt(number)

View File

@@ -13,6 +13,7 @@ import erpnext
class InvalidWarehouseCompany(frappe.ValidationError): pass class InvalidWarehouseCompany(frappe.ValidationError): pass
class PendingRepostingError(frappe.ValidationError): pass
def get_stock_value_from_bin(warehouse=None, item_code=None): def get_stock_value_from_bin(warehouse=None, item_code=None):
values = {} values = {}
@@ -188,7 +189,7 @@ def get_bin(item_code, warehouse):
bin_obj.flags.ignore_permissions = True bin_obj.flags.ignore_permissions = True
return bin_obj return bin_obj
def get_or_make_bin(item_code, warehouse) -> str: def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record: if not bin_record:
@@ -204,11 +205,12 @@ def get_or_make_bin(item_code, warehouse) -> str:
return bin_record return bin_record
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
"""WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.doctype.bin.bin import update_stock from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item: if is_stock_item:
bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
else: else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
@@ -417,3 +419,28 @@ def is_reposting_item_valuation_in_progress():
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
if reposting_in_progress: if reposting_in_progress:
frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
"""Check if there are pending reposting job till the specified posting date."""
filters = {
"docstatus": 1,
"status": ["in", ["Queued","In Progress", "Failed"]],
"posting_date": ["<=", posting_date],
}
reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
if reposting_pending and throw_error:
msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.")
frappe.msgprint(msg,
raise_exception=PendingRepostingError,
title="Stock Reposting Ongoing",
indicator="red",
primary_action={
"label": _("Show pending entries"),
"client_action": "erpnext.route_to_pending_reposts",
"args": filters,
}
)
return bool(reposting_pending)

View File

@@ -5,7 +5,7 @@ import frappe
from frappe.utils import cint, cstr from frappe.utils import cint, cstr
from redisearch import AutoCompleter, Client, Query from redisearch import AutoCompleter, Client, Query
from erpnext.e_commerce.redisearch import ( from erpnext.e_commerce.redisearch_utils import (
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
WEBSITE_ITEM_INDEX, WEBSITE_ITEM_INDEX,
WEBSITE_ITEM_NAME_AUTOCOMPLETE, WEBSITE_ITEM_NAME_AUTOCOMPLETE,

View File

@@ -1847,7 +1847,7 @@ Overdue,Überfällig,
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
Owner,Besitzer, Owner,Besitzer,
PAN,PFANNE, PAN,PAN,
POS,Verkaufsstelle, POS,Verkaufsstelle,
POS Profile,Verkaufsstellen-Profil, POS Profile,Verkaufsstellen-Profil,
POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden", POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden",
Can't render this file because it is too large.

View File

@@ -163,6 +163,28 @@ class TransactionBase(StatusUpdater):
return ret return ret
def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str):
""" Reset "Set default X" fields on forms to avoid confusion.
example:
doc = {
"set_from_warehouse": "Warehouse A",
"items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}],
}
Since this has dissimilar values in child table, the default field will be erased.
doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
"""
child_table_values = set()
for row in self.get(child_table):
child_table_values.add(row.get(child_table_field))
if len(child_table_values) > 1:
self.set(default_field, None)
else:
self.set(default_field, list(child_table_values)[0])
def delete_events(ref_type, ref_name): def delete_events(ref_type, ref_name):
events = frappe.db.sql_list(""" SELECT events = frappe.db.sql_list(""" SELECT
distinct `tabEvent`.name distinct `tabEvent`.name