mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-23 16:48:30 +00:00
chore: merge branch 'version-13-hotfix' into 'version-13-pre-release'
This commit is contained in:
@@ -375,12 +375,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
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
|
||||
else:
|
||||
frappe.db.rollback()
|
||||
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
|
||||
|
||||
def send_mail(deferred_process):
|
||||
@@ -447,10 +448,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
|
||||
if submit:
|
||||
journal_entry.submit()
|
||||
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
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
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
def on_update(self):
|
||||
@@ -25,6 +27,7 @@ class AccountsSettings(Document):
|
||||
self.validate_stale_days()
|
||||
self.enable_payment_schedule_in_print()
|
||||
self.toggle_discount_accounting_fields()
|
||||
self.validate_pending_reposts()
|
||||
|
||||
def validate_stale_days(self):
|
||||
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("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)
|
||||
|
||||
@@ -57,7 +57,8 @@ class GLEntry(Document):
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
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,
|
||||
self.against_voucher)
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@ class JournalEntry(AccountsController):
|
||||
if not frappe.flags.in_import:
|
||||
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()
|
||||
if self.docstatus == 0:
|
||||
self.set_against_account()
|
||||
@@ -68,7 +71,6 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
"column_break10",
|
||||
"amount_eligible_for_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break2",
|
||||
@@ -1561,16 +1562,23 @@
|
||||
"label": "Coupon Code",
|
||||
"options": "Coupon Code",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-27 20:12:57.306772",
|
||||
"modified": "2021-10-05 12:11:53.871828",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"is_free_item",
|
||||
"grant_commission",
|
||||
"section_break_21",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -800,14 +801,22 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-04 17:34:49.924531",
|
||||
"modified": "2021-10-05 12:23:47.506290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
|
||||
{% 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', {
|
||||
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() {
|
||||
return {
|
||||
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 {
|
||||
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) {
|
||||
if(!doc.company) {
|
||||
if (!doc.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);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.company) {
|
||||
if (frm.doc.company) {
|
||||
frm.trigger("toggle_display_account_head");
|
||||
}
|
||||
},
|
||||
@@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', {
|
||||
frm.toggle_display('expense_account',
|
||||
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
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
@@ -112,6 +112,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
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):
|
||||
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
|
||||
|
||||
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)
|
||||
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:
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif not item.expense_account and for_validate:
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
"column_break10",
|
||||
"amount_eligible_for_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break2",
|
||||
@@ -2019,6 +2020,12 @@
|
||||
"label": "Total Billing Hours",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2031,7 +2038,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-11 20:19:38.667508",
|
||||
"modified": "2021-10-21 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2086,4 +2093,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
post_depreciation_entries,
|
||||
make_depreciation_entry,
|
||||
)
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
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:
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def validate_fixed_asset(self):
|
||||
for d in self.get("items"):
|
||||
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)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
else:
|
||||
@@ -1006,22 +1009,20 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
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()
|
||||
|
||||
post_depreciation_entries(self.posting_date)
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# 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)
|
||||
asset.save()
|
||||
|
||||
self.delete_depreciation_entry_made_after_sale(asset)
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
'Asset Repair',
|
||||
@@ -1035,7 +1036,7 @@ class SalesInvoice(SellingController):
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
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
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
@@ -1050,11 +1051,19 @@ class SalesInvoice(SellingController):
|
||||
row += 1
|
||||
|
||||
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.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_sales_invoice(self):
|
||||
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
|
||||
|
||||
@@ -1069,6 +1078,12 @@ class SalesInvoice(SellingController):
|
||||
return True
|
||||
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
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
|
||||
@@ -2204,9 +2204,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
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()
|
||||
@@ -2219,7 +2219,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.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):
|
||||
@@ -2228,6 +2228,59 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
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):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
@@ -2317,6 +2370,29 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
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):
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"grant_commission",
|
||||
"section_break_21",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -745,7 +746,6 @@
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset",
|
||||
"no_copy": 1,
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
@@ -829,15 +829,23 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-19 13:41:53.435827",
|
||||
"modified": "2021-10-05 12:24:54.968907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -109,7 +109,11 @@ class ReceivablePayableReport(object):
|
||||
invoiced = 0.0,
|
||||
paid = 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)
|
||||
|
||||
@@ -150,21 +154,28 @@ class ReceivablePayableReport(object):
|
||||
# gle_balance will be the total "debit - credit" for receivable type reports and
|
||||
# and vice-versa for payable type reports
|
||||
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.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
|
||||
# debit against sales / purchase invoice
|
||||
row.paid -= gle_balance
|
||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
||||
else:
|
||||
# invoice
|
||||
row.invoiced += gle_balance
|
||||
row.invoiced_in_account_currency += gle_balance_in_account_currency
|
||||
else:
|
||||
# payment or credit note for receivables
|
||||
if self.is_invoice(gle):
|
||||
# stand alone debit / credit note
|
||||
row.credit_note -= gle_balance
|
||||
row.credit_note_in_account_currency -= gle_balance_in_account_currency
|
||||
else:
|
||||
# advance / unlinked payment or other adjustment
|
||||
row.paid -= gle_balance
|
||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
||||
|
||||
if 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
|
||||
for key, row in self.voucher_balance.items():
|
||||
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
|
||||
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
|
||||
|
||||
if self.is_invoice(row) and self.filters.based_on_payment_terms:
|
||||
@@ -529,7 +545,9 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def set_ageing(self, row):
|
||||
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":
|
||||
entry_date = row.bill_date
|
||||
else:
|
||||
@@ -583,12 +601,14 @@ class ReceivablePayableReport(object):
|
||||
else:
|
||||
select_fields = "debit, credit"
|
||||
|
||||
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
|
||||
|
||||
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
||||
|
||||
self.gl_entries = frappe.db.sql("""
|
||||
select
|
||||
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
|
||||
`tabGL Entry`
|
||||
where
|
||||
@@ -596,8 +616,8 @@ class ReceivablePayableReport(object):
|
||||
and is_cancelled = 0
|
||||
and party_type=%s
|
||||
and (party is not null and party != '')
|
||||
{1} {2} {3}"""
|
||||
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
|
||||
{2} {3} {4}"""
|
||||
.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):
|
||||
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
|
||||
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):
|
||||
# get "credit" balance if report type is "debit" and vice versa
|
||||
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')
|
||||
|
||||
@@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_zero_values",
|
||||
"label": __("Show zero values"),
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import (
|
||||
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.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 (
|
||||
get_chart_data as get_pl_chart_data,
|
||||
)
|
||||
@@ -265,7 +269,7 @@ def get_columns(companies, filters):
|
||||
return columns
|
||||
|
||||
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)
|
||||
|
||||
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 = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
|
||||
|
||||
if out:
|
||||
add_total_row(out, root_type, balance_must_be, companies, company_currency)
|
||||
|
||||
@@ -370,7 +376,7 @@ def get_account_heads(root_type, companies, filters):
|
||||
|
||||
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):
|
||||
"""Update parent_account_name in accounts list.
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-05-06 12:28:23",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-03-06 05:52:57.645281",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Partners Commission",
|
||||
"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\"",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Partners Commission",
|
||||
"report_type": "Query Report",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-05-06 12:28:23",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-10-06 06:26:07.881340",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Partners Commission",
|
||||
"owner": "Administrator",
|
||||
"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",
|
||||
"report_name": "Sales Partners Commission",
|
||||
"report_type": "Query Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
posting_date = entry.posting_date
|
||||
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:
|
||||
tds_deducted += (entry.credit - entry.debit)
|
||||
|
||||
total_amount_credited += (entry.credit - entry.debit)
|
||||
|
||||
if rate and tds_deducted:
|
||||
if tds_deducted:
|
||||
row = {
|
||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
||||
'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():
|
||||
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:
|
||||
supplier_map[d.name] = d
|
||||
|
||||
@@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
if (frm.doc.docstatus==1) {
|
||||
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);
|
||||
}, __("Manage"));
|
||||
|
||||
frm.add_custom_button("Scrap Asset", function() {
|
||||
frm.add_custom_button(__("Scrap Asset"), function() {
|
||||
erpnext.asset.scrap_asset(frm);
|
||||
}, __("Manage"));
|
||||
|
||||
frm.add_custom_button("Sell Asset", function() {
|
||||
frm.add_custom_button(__("Sell Asset"), function() {
|
||||
frm.trigger("make_sales_invoice");
|
||||
}, __("Manage"));
|
||||
|
||||
} 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);
|
||||
}, __("Manage"));
|
||||
}
|
||||
@@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
if (frm.doc.status != 'Fully Depreciated') {
|
||||
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
||||
frm.trigger("create_asset_adjustment");
|
||||
frm.trigger("create_asset_value_adjustment");
|
||||
}, __("Manage"));
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ frappe.ui.form.on('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 = {
|
||||
"voucher_no": frm.doc.name,
|
||||
"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({
|
||||
args: {
|
||||
"asset": frm.doc.name,
|
||||
"asset_category": frm.doc.asset_category,
|
||||
"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,
|
||||
callback: function(r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
|
||||
@@ -73,12 +73,12 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset and self.purchase_invoice:
|
||||
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:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
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:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
@@ -180,7 +180,7 @@ class Asset(AccountsController):
|
||||
d.precision("rate_of_depreciation"))
|
||||
|
||||
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 = []
|
||||
|
||||
if not self.available_for_use_date:
|
||||
@@ -193,8 +193,7 @@ class Asset(AccountsController):
|
||||
|
||||
# value_after_depreciation - current Asset value
|
||||
if self.docstatus == 1 and d.value_after_depreciation:
|
||||
value_after_depreciation = (flt(d.value_after_depreciation) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
value_after_depreciation = flt(d.value_after_depreciation)
|
||||
else:
|
||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
@@ -230,17 +229,19 @@ class Asset(AccountsController):
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
from_date, date_of_sale)
|
||||
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
})
|
||||
if depreciation_amount > 0:
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# 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,
|
||||
self.available_for_use_date, d.depreciation_start_date)
|
||||
|
||||
@@ -253,13 +254,17 @@ class Asset(AccountsController):
|
||||
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
|
||||
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, 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)
|
||||
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
|
||||
def check_is_pro_rata(self, row):
|
||||
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
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
@@ -359,6 +369,9 @@ class Asset(AccountsController):
|
||||
|
||||
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):
|
||||
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")
|
||||
@@ -395,7 +408,29 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
|
||||
.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']
|
||||
finance_books = []
|
||||
|
||||
@@ -412,7 +447,7 @@ class Asset(AccountsController):
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
|
||||
# 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]
|
||||
depreciation_amount += flt(value_after_depreciation -
|
||||
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))
|
||||
@@ -697,14 +732,14 @@ def create_asset_repair(asset, asset_name):
|
||||
return asset_repair
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_adjustment(asset, asset_category, company):
|
||||
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
|
||||
asset_maintenance.update({
|
||||
def create_asset_value_adjustment(asset, asset_category, company):
|
||||
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||
asset_value_adjustment.update({
|
||||
"asset": asset,
|
||||
"company": company,
|
||||
"asset_category": asset_category
|
||||
})
|
||||
return asset_maintenance
|
||||
return asset_value_adjustment
|
||||
|
||||
@frappe.whitelist()
|
||||
def transfer_asset(args):
|
||||
@@ -826,13 +861,11 @@ def get_total_days(date, frequency):
|
||||
|
||||
@erpnext.allow_regional
|
||||
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 the Depreciation Schedule is being prepared for the first time
|
||||
if not asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (flt(row.value_after_depreciation) -
|
||||
flt(row.expected_value_after_useful_life)) / depreciation_left
|
||||
depreciation_amount = (flt(asset.gross_purchase_amount) -
|
||||
flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair
|
||||
else:
|
||||
|
||||
@@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
je.finance_book = d.finance_book
|
||||
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 = {
|
||||
"account": accumulated_depreciation_account,
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
@@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": depreciation_expense_account,
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
@@ -132,6 +134,20 @@ def get_depreciation_accounts(asset):
|
||||
|
||||
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()
|
||||
def scrap_asset(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', {
|
||||
var d = locals[cdt][cdn];
|
||||
return {
|
||||
"filters": {
|
||||
"root_type": "Expense",
|
||||
"root_type": ["in", ["Expense", "Income"]],
|
||||
"is_group": 0,
|
||||
"company": d.company_name
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ class AssetCategory(Document):
|
||||
|
||||
def validate_account_types(self):
|
||||
account_type_map = {
|
||||
'fixed_asset_account': { 'account_type': 'Fixed Asset' },
|
||||
'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
|
||||
'depreciation_expense_account': { 'root_type': 'Expense' },
|
||||
'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
|
||||
'fixed_asset_account': {'account_type': ['Fixed Asset']},
|
||||
'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
|
||||
'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
|
||||
'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
|
||||
}
|
||||
for d in self.accounts:
|
||||
for fieldname in account_type_map.keys():
|
||||
@@ -53,11 +53,11 @@ class AssetCategory(Document):
|
||||
selected_account = d.get(fieldname)
|
||||
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)
|
||||
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.")
|
||||
.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"))
|
||||
|
||||
def valide_cwip_account(self):
|
||||
|
||||
@@ -60,6 +60,10 @@ frappe.ui.form.on('Asset Repair', {
|
||||
if (frm.doc.repair_status == "Completed") {
|
||||
frm.set_value('completion_date', frappe.datetime.now_datetime());
|
||||
}
|
||||
},
|
||||
|
||||
stock_items_on_form_rendered() {
|
||||
erpnext.setup_serial_or_batch_no();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -118,9 +118,10 @@ class AssetRepair(AccountsController):
|
||||
for stock_item in self.get('stock_items'):
|
||||
stock_entry.append('items', {
|
||||
"s_warehouse": self.warehouse,
|
||||
"item_code": stock_item.item,
|
||||
"item_code": stock_item.item_code,
|
||||
"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()
|
||||
|
||||
@@ -11,12 +11,15 @@ from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset_data,
|
||||
set_depreciation_settings_in_company,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestAssetRepair(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
set_depreciation_settings_in_company()
|
||||
create_asset_data()
|
||||
create_item("_Test Stock Item")
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
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.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)
|
||||
|
||||
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):
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
@@ -137,11 +159,12 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.stock_consumption:
|
||||
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", {
|
||||
"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,
|
||||
"consumed_quantity": args.qty or 1
|
||||
"consumed_quantity": args.qty or 1,
|
||||
"serial_no": args.serial_no
|
||||
})
|
||||
|
||||
asset_repair.insert(ignore_if_duplicate=True)
|
||||
@@ -158,7 +181,7 @@ def create_asset_repair(**args):
|
||||
})
|
||||
stock_entry.append('items', {
|
||||
"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
|
||||
})
|
||||
stock_entry.submit()
|
||||
|
||||
@@ -5,19 +5,13 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item",
|
||||
"item_code",
|
||||
"valuation_rate",
|
||||
"consumed_quantity",
|
||||
"total_value"
|
||||
"total_value",
|
||||
"serial_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "item",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item.valuation_rate",
|
||||
"fieldname": "valuation_rate",
|
||||
@@ -38,12 +32,24 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Total Value",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-12 03:19:55.006300",
|
||||
"modified": "2021-11-11 18:23:00.492483",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
|
||||
@@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.create_raw_materials_supplied("supplied_items")
|
||||
self.set_received_qty_for_drop_ship_items()
|
||||
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):
|
||||
super(PurchaseOrder, self).validate_with_previous_doc({
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"company",
|
||||
"transaction_date",
|
||||
"valid_till",
|
||||
"quotation_number",
|
||||
"amended_from",
|
||||
"address_section",
|
||||
"supplier_address",
|
||||
@@ -797,6 +798,11 @@
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Valid Till"
|
||||
},
|
||||
{
|
||||
"fieldname": "quotation_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Quotation Number"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
@@ -804,10 +810,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 00:58:20.995491",
|
||||
"modified": "2021-12-11 06:43:20.924080",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -256,7 +256,12 @@ class AccountsController(TransactionBase):
|
||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
||||
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_contribution()
|
||||
|
||||
|
||||
@@ -120,13 +120,27 @@ class SellingController(StockController):
|
||||
self.in_words = money_in_words(amount, self.currency)
|
||||
|
||||
def calculate_commission(self):
|
||||
if self.meta.get_field("commission_rate"):
|
||||
self.round_floats_in(self, ["base_net_total", "commission_rate"])
|
||||
if self.commission_rate > 100.0:
|
||||
throw(_("Commission rate cannot be greater than 100"))
|
||||
if not self.meta.get_field("commission_rate"):
|
||||
return
|
||||
|
||||
self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0,
|
||||
self.precision("total_commission"))
|
||||
self.round_floats_in(
|
||||
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):
|
||||
if not self.meta.get_field("sales_team"):
|
||||
@@ -138,7 +152,7 @@ class SellingController(StockController):
|
||||
self.round_floats_in(sales_person)
|
||||
|
||||
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))
|
||||
|
||||
if sales_person.commission_rate:
|
||||
|
||||
22
erpnext/controllers/tests/test_transaction_base.py
Normal file
22
erpnext/controllers/tests/test_transaction_base.py
Normal 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)
|
||||
|
||||
@@ -6,7 +6,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
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,
|
||||
get_indexable_web_fields,
|
||||
is_search_module_loaded,
|
||||
|
||||
@@ -11,7 +11,7 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
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,
|
||||
insert_item_to_index,
|
||||
update_index_for_item,
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"configuration_cb",
|
||||
"shipping_account_head",
|
||||
"section_break_12",
|
||||
"nexus_address",
|
||||
"nexus"
|
||||
],
|
||||
"fields": [
|
||||
@@ -87,15 +86,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "nexus",
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Nexus List"
|
||||
},
|
||||
{
|
||||
"fieldname": "nexus_address",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Nexus Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "nexus",
|
||||
"fieldtype": "Table",
|
||||
@@ -107,20 +102,21 @@
|
||||
"fieldname": "configuration_cb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-08 18:02:29.232090",
|
||||
"migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
|
||||
"modified": "2021-11-30 11:17:24.647979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "TaxJar Settings",
|
||||
|
||||
@@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client
|
||||
class TaxJarSettings(Document):
|
||||
|
||||
def on_update(self):
|
||||
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
|
||||
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
|
||||
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
|
||||
TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
|
||||
TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
|
||||
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_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
|
||||
|
||||
@@ -273,6 +273,9 @@ doc_events = {
|
||||
"erpnext.regional.india.utils.update_taxable_values"
|
||||
]
|
||||
},
|
||||
"POS Invoice": {
|
||||
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
"validate": [
|
||||
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
|
||||
|
||||
@@ -96,15 +96,8 @@ class Employee(NestedSet):
|
||||
'user': self.user_id
|
||||
})
|
||||
|
||||
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
|
||||
if employee_user_permission_exists:
|
||||
return
|
||||
|
||||
add_user_permission("Employee", self.name, self.user_id)
|
||||
set_user_permission_if_allowed("Company", self.company, self.user_id)
|
||||
|
||||
@@ -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_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')
|
||||
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, {
|
||||
"Maintenance Schedule": {
|
||||
@@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
|
||||
"Maintenance Schedule Item": {
|
||||
"doctype": "Maintenance Visit Purpose",
|
||||
"condition": lambda doc: doc.item_name == item_name,
|
||||
"postprocess": update_sales
|
||||
"postprocess": update_sales_and_serial
|
||||
}
|
||||
}, target_doc)
|
||||
|
||||
|
||||
@@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', {
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
frm.clear_table("purposes");
|
||||
}
|
||||
|
||||
if (!frm.doc.status) {
|
||||
frm.set_value({ status: 'Draft' });
|
||||
}
|
||||
if (frm.doc.__islocal) {
|
||||
frm.clear_table("purposes");
|
||||
frm.set_value({ mntc_date: frappe.datetime.get_today() });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,10 +5,17 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'job_card',
|
||||
'non_standard_fieldnames': {
|
||||
'Quality Inspection': 'reference_name'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Transactions'),
|
||||
'items': ['Material Request', 'Stock Entry']
|
||||
},
|
||||
{
|
||||
'label': _('Reference'),
|
||||
'items': ['Quality Inspection']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -292,7 +292,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.update_job_card_details
|
||||
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.update_subscription_status_in_memberships
|
||||
erpnext.patches.v13_0.update_amt_in_work_order_required_items
|
||||
@@ -304,6 +304,7 @@ erpnext.patches.v13_0.update_recipient_email_digest
|
||||
erpnext.patches.v13_0.shopify_deprecation_warning
|
||||
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
|
||||
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
|
||||
erpnext.patches.v13_0.remove_bad_selling_defaults
|
||||
erpnext.patches.v13_0.migrate_stripe_api
|
||||
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
|
||||
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
|
||||
@@ -334,3 +335,5 @@ erpnext.patches.v12_0.update_production_plan_status
|
||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
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
|
||||
|
||||
@@ -9,24 +9,29 @@ def execute():
|
||||
frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
|
||||
frappe.reload_doc('accounts', 'doctype', 'payment_entry')
|
||||
|
||||
custom_fields = {
|
||||
'Payment Entry': [
|
||||
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
|
||||
print_hide=1, collapsible=1),
|
||||
dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
|
||||
print_hide=1, options='Address'),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', insert_after='company_address',
|
||||
fetch_from='company_address.gstin', print_hide=1, read_only=1),
|
||||
dict(fieldname='place_of_supply', label='Place of Supply',
|
||||
fieldtype='Data', insert_after='company_gstin',
|
||||
print_hide=1, read_only=1),
|
||||
dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
|
||||
print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
|
||||
dict(fieldname='customer_gstin', label='Customer GSTIN',
|
||||
fieldtype='Data', insert_after='customer_address',
|
||||
fetch_from='customer_address.gstin', print_hide=1, read_only=1)
|
||||
]
|
||||
}
|
||||
if frappe.db.exists('Company', {'country': 'India'}):
|
||||
custom_fields = {
|
||||
'Payment Entry': [
|
||||
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
|
||||
print_hide=1, collapsible=1),
|
||||
dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
|
||||
print_hide=1, options='Address'),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', insert_after='company_address',
|
||||
fetch_from='company_address.gstin', print_hide=1, read_only=1),
|
||||
dict(fieldname='place_of_supply', label='Place of Supply',
|
||||
fieldtype='Data', insert_after='company_gstin',
|
||||
print_hide=1, read_only=1),
|
||||
dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
|
||||
print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
|
||||
dict(fieldname='customer_gstin', label='Customer GSTIN',
|
||||
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}")
|
||||
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal file
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2020, Wahni Green Technologies and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
|
||||
if company:
|
||||
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)
|
||||
16
erpnext/patches/v13_0/remove_bad_selling_defaults.py
Normal file
16
erpnext/patches/v13_0/remove_bad_selling_defaults.py
Normal 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)
|
||||
16
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal file
16
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2020, Wahni Green Technologies and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
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)
|
||||
if frappe.db.has_column('Sales Invoice', 'qr_code'):
|
||||
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
|
||||
@@ -1086,7 +1086,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
$.each(this.frm.doc.taxes || [], function(i, d) {
|
||||
if(d.charge_type == "Actual") {
|
||||
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));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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: () => {
|
||||
frappe.ui.hide_open_dialog();
|
||||
|
||||
|
||||
@@ -495,6 +495,11 @@
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
> .item-qty-total-container {
|
||||
@extend .net-total-container;
|
||||
padding: 5px 0px 0px 0px;
|
||||
}
|
||||
|
||||
> .taxes-container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document):
|
||||
memberships = frappe.db.get_all('Membership', {
|
||||
'member': self.member,
|
||||
'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')
|
||||
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
|
||||
|
||||
|
||||
@@ -561,17 +561,17 @@ def get_item_list(data, doc, hsn_wise=False):
|
||||
}
|
||||
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)
|
||||
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()
|
||||
if not hsn_code:
|
||||
if not item_or_hsn:
|
||||
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.qtyUnit = ""
|
||||
for attr in item_data_attrs:
|
||||
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, '')
|
||||
for tax_acc, attrs in tax_map.items():
|
||||
if account_type == tax_acc:
|
||||
@@ -839,13 +839,11 @@ def update_taxable_values(doc, method):
|
||||
doc.get('items')[item_count - 1].taxable_value += diff
|
||||
|
||||
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 the Depreciation Schedule is being prepared for the first time
|
||||
if not asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (flt(row.value_after_depreciation) -
|
||||
flt(row.expected_value_after_useful_life)) / depreciation_left
|
||||
depreciation_amount = (flt(asset.gross_purchase_amount) -
|
||||
flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair
|
||||
else:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
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 frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
@@ -13,6 +13,16 @@ def setup(company=None, patch=True):
|
||||
add_permissions()
|
||||
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():
|
||||
"""Add Permissions for KSA VAT Setting."""
|
||||
add_permission('KSA VAT Setting', 'All', 0)
|
||||
@@ -33,8 +43,16 @@ def make_custom_fields():
|
||||
custom_fields = {
|
||||
'Sales Invoice': [
|
||||
dict(
|
||||
fieldname='qr_code',
|
||||
label='QR Code',
|
||||
fieldname='ksa_einv_qr',
|
||||
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',
|
||||
read_only=1, no_copy=1, hidden=1
|
||||
)
|
||||
|
||||
@@ -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 os
|
||||
from base64 import b64encode
|
||||
|
||||
import frappe
|
||||
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 pyqrcode import create as qr_create
|
||||
|
||||
from erpnext import get_region
|
||||
|
||||
|
||||
def create_qr_code(doc, method):
|
||||
"""Create QR Code after inserting Sales Inv
|
||||
"""
|
||||
|
||||
def create_qr_code(doc, method=None):
|
||||
region = get_region(doc.company)
|
||||
if region not in ['Saudi Arabia']:
|
||||
return
|
||||
|
||||
# if QR Code field not present, do nothing
|
||||
if not hasattr(doc, 'qr_code'):
|
||||
return
|
||||
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
|
||||
if not hasattr(doc, 'ksa_einv_qr'):
|
||||
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
|
||||
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}):
|
||||
return
|
||||
|
||||
meta = frappe.get_meta('Sales Invoice')
|
||||
meta = frappe.get_meta(doc.doctype)
|
||||
|
||||
for field in meta.get_image_fields():
|
||||
if field.fieldname == 'qr_code':
|
||||
''' TLV conversion for
|
||||
1. Seller's Name
|
||||
2. VAT Number
|
||||
3. Time Stamp
|
||||
4. Invoice Amount
|
||||
5. VAT Amount
|
||||
'''
|
||||
tlv_array = []
|
||||
# Sellers Name
|
||||
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
|
||||
''' TLV conversion for
|
||||
1. Seller's Name
|
||||
2. VAT Number
|
||||
3. Time Stamp
|
||||
4. Invoice Amount
|
||||
5. VAT Amount
|
||||
'''
|
||||
tlv_array = []
|
||||
# Sellers Name
|
||||
|
||||
seller_name = frappe.db.get_value(
|
||||
'Company',
|
||||
doc.company,
|
||||
'company_name_in_arabic')
|
||||
seller_name = frappe.db.get_value(
|
||||
'Company',
|
||||
doc.company,
|
||||
'company_name_in_arabic')
|
||||
|
||||
if not seller_name:
|
||||
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
||||
if not seller_name:
|
||||
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
||||
|
||||
tag = bytes([1]).hex()
|
||||
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
||||
value = seller_name.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([1]).hex()
|
||||
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
||||
value = seller_name.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# VAT Number
|
||||
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
||||
if not tax_id:
|
||||
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
||||
# VAT Number
|
||||
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
||||
if not tax_id:
|
||||
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
||||
|
||||
tag = bytes([2]).hex()
|
||||
length = bytes([len(tax_id)]).hex()
|
||||
value = tax_id.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([2]).hex()
|
||||
length = bytes([len(tax_id)]).hex()
|
||||
value = tax_id.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# Time Stamp
|
||||
posting_date = getdate(doc.posting_date)
|
||||
time = get_time(doc.posting_time)
|
||||
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
||||
time_stamp = add_to_date(posting_date, seconds=seconds)
|
||||
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
# Time Stamp
|
||||
posting_date = getdate(doc.posting_date)
|
||||
time = get_time(doc.posting_time)
|
||||
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
||||
time_stamp = add_to_date(posting_date, seconds=seconds)
|
||||
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
tag = bytes([3]).hex()
|
||||
length = bytes([len(time_stamp)]).hex()
|
||||
value = time_stamp.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([3]).hex()
|
||||
length = bytes([len(time_stamp)]).hex()
|
||||
value = time_stamp.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# Invoice Amount
|
||||
invoice_amount = str(doc.grand_total)
|
||||
tag = bytes([4]).hex()
|
||||
length = bytes([len(invoice_amount)]).hex()
|
||||
value = invoice_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
# Invoice Amount
|
||||
invoice_amount = str(doc.grand_total)
|
||||
tag = bytes([4]).hex()
|
||||
length = bytes([len(invoice_amount)]).hex()
|
||||
value = invoice_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# VAT Amount
|
||||
vat_amount = str(doc.total_taxes_and_charges)
|
||||
# VAT Amount
|
||||
vat_amount = str(doc.total_taxes_and_charges)
|
||||
|
||||
tag = bytes([5]).hex()
|
||||
length = bytes([len(vat_amount)]).hex()
|
||||
value = vat_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([5]).hex()
|
||||
length = bytes([len(vat_amount)]).hex()
|
||||
value = vat_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# Joining bytes into one
|
||||
tlv_buff = ''.join(tlv_array)
|
||||
# Joining bytes into one
|
||||
tlv_buff = ''.join(tlv_array)
|
||||
|
||||
# base64 conversion for QR Code
|
||||
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
||||
# base64 conversion for QR Code
|
||||
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
||||
|
||||
qr_image = io.BytesIO()
|
||||
url = qr_create(base64_string, error='L')
|
||||
url.png(qr_image, scale=2, quiet_zone=1)
|
||||
qr_image = io.BytesIO()
|
||||
url = qr_create(base64_string, error='L')
|
||||
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
|
||||
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"is_private": 0,
|
||||
"content": qr_image.getvalue(),
|
||||
"attached_to_doctype": doc.get("doctype"),
|
||||
"attached_to_name": doc.get("name"),
|
||||
"attached_to_field": "qr_code"
|
||||
})
|
||||
# making file
|
||||
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"is_private": 0,
|
||||
"content": qr_image.getvalue(),
|
||||
"attached_to_doctype": doc.get("doctype"),
|
||||
"attached_to_name": doc.get("name"),
|
||||
"attached_to_field": "ksa_einv_qr"
|
||||
})
|
||||
|
||||
_file.save()
|
||||
_file.save()
|
||||
|
||||
# assigning to document
|
||||
doc.db_set('qr_code', _file.file_url)
|
||||
doc.notify_update()
|
||||
|
||||
break
|
||||
# assigning to document
|
||||
doc.db_set('ksa_einv_qr', _file.file_url)
|
||||
doc.notify_update()
|
||||
|
||||
|
||||
def delete_qr_code_file(doc, method):
|
||||
"""Delete QR Code on deleted sales invoice"""
|
||||
|
||||
def delete_qr_code_file(doc, method=None):
|
||||
region = get_region(doc.company)
|
||||
if region not in ['Saudi Arabia']:
|
||||
return
|
||||
|
||||
if hasattr(doc, 'qr_code'):
|
||||
if doc.get('qr_code'):
|
||||
if hasattr(doc, 'ksa_einv_qr'):
|
||||
if doc.get('ksa_einv_qr'):
|
||||
file_doc = frappe.get_list('File', {
|
||||
'file_url': doc.get('qr_code')
|
||||
'file_url': doc.get('ksa_einv_qr')
|
||||
})
|
||||
if len(file_doc):
|
||||
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':
|
||||
return
|
||||
|
||||
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
|
||||
settings_doc.delete()
|
||||
if frappe.db.exists('KSA VAT Setting', doc.name):
|
||||
frappe.delete_doc('KSA VAT Setting', doc.name)
|
||||
|
||||
@@ -952,8 +952,7 @@
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"max_attachments": 1,
|
||||
"modified": "2021-08-27 20:10:07.864951",
|
||||
"modified": "2021-11-30 01:33:21.106073",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
"column_break7",
|
||||
"amount_eligible_for_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break1",
|
||||
@@ -1507,16 +1508,23 @@
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Dispatch Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 13:09:51.515542",
|
||||
"modified": "2021-10-05 12:16:40.775704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -64,6 +64,8 @@ class SalesOrder(SellingController):
|
||||
if not self.billing_status: self.billing_status = 'Not Billed'
|
||||
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
||||
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def validate_po(self):
|
||||
# validate p.o date v/s delivery date
|
||||
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']
|
||||
)).insert()
|
||||
work_order.set_work_order_operations()
|
||||
work_order.flags.ignore_mandatory = True
|
||||
work_order.save()
|
||||
out.append(work_order)
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"grant_commission",
|
||||
"section_break_24",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -789,15 +790,23 @@
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-23 01:15:05.803091",
|
||||
"modified": "2021-10-05 12:27:25.014789",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -8,7 +8,6 @@ import frappe
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
|
||||
class SellingSettings(Document):
|
||||
@@ -37,9 +36,3 @@ class SellingSettings(Document):
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
@@ -100,6 +100,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
`<div class="add-discount-wrapper">
|
||||
${this.get_discount_icon()} ${__('Add Discount')}
|
||||
</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-label">${__("Net Total")}</div>
|
||||
<div class="net-total-value">0.00</div>
|
||||
@@ -142,6 +146,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
this.$numpad_section.prepend(
|
||||
`<div class="numpad-totals">
|
||||
<span class="numpad-item-qty-total"></span>
|
||||
<span class="numpad-net-total"></span>
|
||||
<span class="numpad-grand-total"></span>
|
||||
</div>`
|
||||
@@ -470,6 +475,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
if (!frm) frm = this.events.get_frm();
|
||||
|
||||
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;
|
||||
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) {
|
||||
const currency = this.events.get_frm().doc.currency;
|
||||
this.$totals_section.find('.grand-total-container').html(
|
||||
|
||||
@@ -157,25 +157,19 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
|
||||
commission_rate: function() {
|
||||
this.calculate_commission();
|
||||
refresh_field("total_commission");
|
||||
},
|
||||
|
||||
total_commission: function() {
|
||||
if(this.frm.doc.base_net_total) {
|
||||
frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]);
|
||||
frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
|
||||
|
||||
if(this.frm.doc.base_net_total < this.frm.doc.total_commission) {
|
||||
var msg = (__("[Error]") + " " +
|
||||
__(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;
|
||||
}
|
||||
const { amount_eligible_for_commission } = this.frm.doc;
|
||||
if (!amount_eligible_for_commission) return;
|
||||
|
||||
this.frm.set_value("commission_rate",
|
||||
flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total));
|
||||
}
|
||||
this.frm.set_value(
|
||||
"commission_rate", flt(
|
||||
this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
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,
|
||||
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,
|
||||
precision("allocated_amount", sales_person));
|
||||
refresh_field(["allocated_amount"], sales_person);
|
||||
@@ -259,28 +253,39 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
},
|
||||
|
||||
calculate_commission: function() {
|
||||
if(this.frm.fields_dict.commission_rate) {
|
||||
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;
|
||||
}
|
||||
if (!this.frm.fields_dict.commission_rate) return;
|
||||
|
||||
this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0,
|
||||
precision("total_commission"));
|
||||
if (this.frm.doc.commission_rate > 100) {
|
||||
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() {
|
||||
var me = this;
|
||||
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
|
||||
frappe.model.round_floats_in(sales_person);
|
||||
if(sales_person.allocated_percentage) {
|
||||
sales_person.allocated_amount = flt(
|
||||
me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0,
|
||||
precision("allocated_amount", sales_person));
|
||||
}
|
||||
if (!sales_person.allocated_percentage) return;
|
||||
|
||||
sales_person.allocated_amount = flt(
|
||||
me.frm.doc.amount_eligible_for_commission
|
||||
* sales_person.allocated_percentage
|
||||
/ 100.0,
|
||||
precision("allocated_amount", sales_person)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ frappe.ui.form.on("Company", {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
frm.call('check_if_transactions_exist').then(r => {
|
||||
frm.toggle_enable("default_currency", (!r.message));
|
||||
});
|
||||
},
|
||||
setup: function(frm) {
|
||||
erpnext.company.setup_queries(frm);
|
||||
@@ -87,9 +91,6 @@ frappe.ui.form.on("Company", {
|
||||
|
||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
|
||||
|
||||
frm.toggle_enable("default_currency", (frm.doc.__onload &&
|
||||
!frm.doc.__onload.transactions_exist));
|
||||
|
||||
if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
|
||||
frm.add_custom_button(__('Cost Centers'), function() {
|
||||
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});
|
||||
|
||||
@@ -24,8 +24,8 @@ class Company(NestedSet):
|
||||
|
||||
def onload(self):
|
||||
load_address_and_contact(self, "company")
|
||||
self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist()
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_if_transactions_exist(self):
|
||||
exists = False
|
||||
for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation",
|
||||
|
||||
@@ -304,7 +304,6 @@ def set_more_defaults():
|
||||
|
||||
def update_selling_defaults():
|
||||
selling_settings = frappe.get_doc("Selling Settings")
|
||||
selling_settings.set_default_customer_group_and_territory()
|
||||
selling_settings.cust_master_name = "Customer Name"
|
||||
selling_settings.so_required = "No"
|
||||
selling_settings.dn_required = "No"
|
||||
|
||||
@@ -53,6 +53,7 @@ def before_tests():
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
||||
enable_all_roles_and_domains()
|
||||
set_defaults_for_tests()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -127,6 +128,14 @@ def enable_all_roles_and_domains():
|
||||
[d.name for d in domains])
|
||||
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):
|
||||
for r in records:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
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):
|
||||
'''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)
|
||||
|
||||
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)
|
||||
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
def get_bin_details(bin_name):
|
||||
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
|
||||
@@ -160,4 +138,4 @@ def update_qty(bin_name, args):
|
||||
'indented_qty': indented_qty,
|
||||
'planned_qty': planned_qty,
|
||||
'projected_qty': projected_qty
|
||||
})
|
||||
})
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
"column_break7",
|
||||
"amount_eligible_for_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break1",
|
||||
@@ -1302,16 +1303,23 @@
|
||||
"label": "Dispatch Address",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-08 14:29:13.428984",
|
||||
"modified": "2021-10-09 14:29:13.428984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -138,6 +138,7 @@ class DeliveryNote(SellingController):
|
||||
self.update_current_stock()
|
||||
|
||||
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):
|
||||
super(DeliveryNote, self).validate_with_previous_doc({
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"grant_commission",
|
||||
"section_break_25",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -753,13 +754,20 @@
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-05 12:12:44.018872",
|
||||
"modified": "2021-10-06 12:12:44.018872",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"sales_details",
|
||||
"sales_uom",
|
||||
"is_sales_item",
|
||||
"grant_commission",
|
||||
"column_break3",
|
||||
"max_discount",
|
||||
"deferred_revenue",
|
||||
@@ -942,6 +943,12 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Published in Website",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-tag",
|
||||
@@ -949,8 +956,7 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 1,
|
||||
"modified": "2021-09-10 12:23:07.277077",
|
||||
"modified": "2021-12-03 08:32:03.869294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
@@ -1017,7 +1023,7 @@
|
||||
"search_fields": "item_name,description,item_group,customer_code",
|
||||
"show_name_in_global_search": 1,
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "idx desc,modified desc",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "item_name",
|
||||
"track_changes": 1
|
||||
|
||||
@@ -496,7 +496,6 @@ class Item(Document):
|
||||
|
||||
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)
|
||||
|
||||
@@ -510,7 +509,6 @@ class Item(Document):
|
||||
repost_stock(new_name, warehouse)
|
||||
|
||||
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):
|
||||
if self.is_new():
|
||||
|
||||
@@ -81,6 +81,9 @@ class MaterialRequest(BuyingController):
|
||||
# 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
|
||||
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
def set_title(self):
|
||||
'''Set title as comma separated list of items'''
|
||||
if not self.title:
|
||||
|
||||
@@ -119,6 +119,10 @@ class PurchaseReceipt(BuyingController):
|
||||
if getdate(self.posting_date) > getdate(nowdate()):
|
||||
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):
|
||||
for item in self.get('items'):
|
||||
|
||||
@@ -168,8 +168,8 @@ def repost_entries():
|
||||
for row in riv_entries:
|
||||
doc = frappe.get_doc('Repost Item Valuation', row.name)
|
||||
if doc.status in ('Queued', 'In Progress'):
|
||||
doc.deduplicate_similar_repost()
|
||||
repost(doc)
|
||||
doc.deduplicate_similar_repost()
|
||||
|
||||
riv_entries = get_repost_item_valuation_entries()
|
||||
if riv_entries:
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
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.repost_item_valuation.repost_item_valuation import (
|
||||
in_configured_timeslot,
|
||||
)
|
||||
from erpnext.stock.utils import PendingRepostingError
|
||||
|
||||
|
||||
class TestRepostItemValuation(unittest.TestCase):
|
||||
@@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase):
|
||||
# to avoid breaking other tests accidentaly
|
||||
riv4.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")
|
||||
|
||||
@@ -104,6 +104,8 @@ class StockEntry(StockController):
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -25,7 +25,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
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):
|
||||
@@ -39,7 +40,7 @@ def get_sle(**args):
|
||||
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
|
||||
values, as_dict=1)
|
||||
|
||||
class TestStockEntry(unittest.TestCase):
|
||||
class TestStockEntry(ERPNextTestCase):
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
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]
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
se = frappe.copy_doc(test_records[0])
|
||||
@@ -999,3 +1077,31 @@ def get_multiple_items():
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
@@ -9,7 +9,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.role.role import get_users
|
||||
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.controllers.item_variant import ItemTemplateCannotHaveStock
|
||||
@@ -44,7 +44,6 @@ class StockLedgerEntry(Document):
|
||||
|
||||
def on_submit(self):
|
||||
self.check_stock_frozen_date()
|
||||
self.actual_amt_check()
|
||||
self.calculate_batch_qty()
|
||||
|
||||
if not self.get("via_landed_cost_voucher"):
|
||||
@@ -58,18 +57,6 @@ class StockLedgerEntry(Document):
|
||||
"sum(actual_qty)") or 0
|
||||
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):
|
||||
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
|
||||
for k in mandatory:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"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.",
|
||||
@@ -153,11 +154,12 @@
|
||||
"icon": "fa fa-upload-alt",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"max_attachments": 1,
|
||||
"modified": "2020-04-08 17:02:47.196206",
|
||||
"links": [],
|
||||
"modified": "2021-11-30 01:33:51.437194",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,8 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.html_utils import clean_html
|
||||
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class StockSettings(Document):
|
||||
def validate(self):
|
||||
@@ -36,6 +38,7 @@ class StockSettings(Document):
|
||||
self.validate_warehouses()
|
||||
self.cant_change_valuation_method()
|
||||
self.validate_clean_description_html()
|
||||
self.validate_pending_reposts()
|
||||
|
||||
def validate_warehouses(self):
|
||||
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
||||
@@ -64,6 +67,11 @@ class StockSettings(Document):
|
||||
# changed to text
|
||||
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):
|
||||
self.toggle_warehouse_field_for_inter_warehouse_transfer()
|
||||
|
||||
|
||||
@@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase):
|
||||
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
|
||||
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):
|
||||
company = "_Test Company"
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-03-07 18:50:32",
|
||||
"description": "A logical Warehouse against which stock entries are made.",
|
||||
"doctype": "DocType",
|
||||
@@ -245,7 +244,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-09 19:54:56.263965",
|
||||
"modified": "2021-12-03 04:40:06.414630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock import get_warehouse_account
|
||||
|
||||
|
||||
@@ -68,57 +67,6 @@ class Warehouse(NestedSet):
|
||||
return frappe.db.sql("""select name from `tabWarehouse`
|
||||
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):
|
||||
if self.is_group:
|
||||
self.convert_to_ledger()
|
||||
|
||||
@@ -328,7 +328,8 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"against_blanket_order": args.get("against_blanket_order"),
|
||||
"bom_no": item.get("default_bom"),
|
||||
"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"):
|
||||
|
||||
@@ -7,10 +7,11 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
from erpnext.stock.utils import (
|
||||
get_incoming_outgoing_rate_for_cancel,
|
||||
get_or_make_bin,
|
||||
@@ -18,19 +19,15 @@ from erpnext.stock.utils import (
|
||||
)
|
||||
|
||||
|
||||
# future reposting
|
||||
class NegativeStockError(frappe.ValidationError): pass
|
||||
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
_exceptions = frappe.local('stockledger_exceptions')
|
||||
# _exceptions = []
|
||||
|
||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
if sl_entries:
|
||||
from erpnext.stock.utils import update_bin
|
||||
|
||||
cancel = sl_entries[0].get("is_cancelled")
|
||||
if cancel:
|
||||
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
|
||||
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):
|
||||
return frappe._dict({
|
||||
@@ -795,10 +823,10 @@ class update_entries_after(object):
|
||||
|
||||
def update_bin(self):
|
||||
# update bin for each warehouse
|
||||
for warehouse, data in iteritems(self.data):
|
||||
bin_record = get_or_make_bin(self.item_code, warehouse)
|
||||
for warehouse, data in self.data.items():
|
||||
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,
|
||||
"actual_qty": data.qty_after_transaction,
|
||||
"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) \
|
||||
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:
|
||||
sle = get_future_sle_with_negative_qty(args)
|
||||
if sle:
|
||||
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
||||
abs(sle[0]["qty_after_transaction"]),
|
||||
frappe.get_desk_link('Item', args.item_code),
|
||||
frappe.get_desk_link('Warehouse', args.warehouse),
|
||||
sle[0]["posting_date"], sle[0]["posting_time"],
|
||||
frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
|
||||
if allow_negative_stock:
|
||||
return
|
||||
if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
|
||||
return
|
||||
|
||||
neg_sle = get_future_sle_with_negative_qty(args)
|
||||
if neg_sle:
|
||||
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
||||
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):
|
||||
return frappe.db.sql("""
|
||||
@@ -1083,6 +1130,29 @@ def get_future_sle_with_negative_qty(args):
|
||||
limit 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:
|
||||
""" Rounds off the number to zero only if number is close to zero for decimal
|
||||
specified in precision. Precision defaults to 6.
|
||||
|
||||
@@ -13,6 +13,7 @@ import erpnext
|
||||
|
||||
|
||||
class InvalidWarehouseCompany(frappe.ValidationError): pass
|
||||
class PendingRepostingError(frappe.ValidationError): pass
|
||||
|
||||
def get_stock_value_from_bin(warehouse=None, item_code=None):
|
||||
values = {}
|
||||
@@ -188,7 +189,7 @@ def get_bin(item_code, warehouse):
|
||||
bin_obj.flags.ignore_permissions = True
|
||||
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})
|
||||
|
||||
if not bin_record:
|
||||
@@ -204,11 +205,12 @@ def get_or_make_bin(item_code, warehouse) -> str:
|
||||
return bin_record
|
||||
|
||||
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
|
||||
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
|
||||
if is_stock_item:
|
||||
bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||
update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
|
||||
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||
update_stock(bin_name, 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")))
|
||||
|
||||
@@ -417,3 +419,28 @@ def is_reposting_item_valuation_in_progress():
|
||||
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||
if reposting_in_progress:
|
||||
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)
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from frappe.utils import cint, cstr
|
||||
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_INDEX,
|
||||
WEBSITE_ITEM_NAME_AUTOCOMPLETE,
|
||||
|
||||
@@ -1847,7 +1847,7 @@ Overdue,Überfällig,
|
||||
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
|
||||
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
|
||||
Owner,Besitzer,
|
||||
PAN,PFANNE,
|
||||
PAN,PAN,
|
||||
POS,Verkaufsstelle,
|
||||
POS Profile,Verkaufsstellen-Profil,
|
||||
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.
|
@@ -163,6 +163,28 @@ class TransactionBase(StatusUpdater):
|
||||
|
||||
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):
|
||||
events = frappe.db.sql_list(""" SELECT
|
||||
distinct `tabEvent`.name
|
||||
|
||||
Reference in New Issue
Block a user