Merge branch 'develop' into item-variants-perf
This commit is contained in:
commit
201a38bfbf
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -30,6 +30,7 @@ issues:
|
||||
exemptLabels:
|
||||
- valid
|
||||
- to-validate
|
||||
- QA
|
||||
markComment: >
|
||||
This issue has been automatically marked as inactive because it has not had
|
||||
recent activity and it wasn't validated by maintainer team. It will be
|
||||
|
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
@ -0,0 +1,44 @@
|
||||
describe("Bulk Transaction Processing", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
it("Creates To Sales Order", () => {
|
||||
cy.visit("/app/sales-order");
|
||||
cy.url().should("include", "/sales-order");
|
||||
cy.window()
|
||||
.its("frappe.csrf_token")
|
||||
.then((csrf_token) => {
|
||||
return cy
|
||||
.request({
|
||||
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Frappe-CSRF-Token": csrf_token,
|
||||
},
|
||||
timeout: 60000,
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.status).eq(200);
|
||||
});
|
||||
});
|
||||
cy.wait(5000);
|
||||
cy.get(
|
||||
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
|
||||
).check({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
|
||||
.contains("Sales Invoice")
|
||||
.click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".modal-content > .modal-footer > .standard-actions")
|
||||
.contains("Yes")
|
||||
.click({ force: true });
|
||||
cy.contains("Creation of Sales Invoice successful");
|
||||
});
|
||||
});
|
@ -2,8 +2,6 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
def get_default_company(user=None):
|
||||
@ -121,14 +119,17 @@ def allow_regional(fn):
|
||||
@erpnext.allow_regional
|
||||
def myfunction():
|
||||
pass'''
|
||||
|
||||
def caller(*args, **kwargs):
|
||||
region = get_region()
|
||||
fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__
|
||||
if region in regional_overrides and fn_name in regional_overrides[region]:
|
||||
return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs)
|
||||
else:
|
||||
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
|
||||
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
|
||||
|
||||
if not overrides or function_path not in overrides:
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
# Priority given to last installed app
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
def get_last_membership(member):
|
||||
|
@ -7,35 +7,30 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"column_break_11",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"invoice_and_billing_tab",
|
||||
"enable_features_section",
|
||||
"unlink_payment_on_cancellation_of_invoice",
|
||||
"automatically_fetch_payment_terms",
|
||||
"delete_linked_ledger_entries",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"column_break_13",
|
||||
"delete_linked_ledger_entries",
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"post_change_gl_entries",
|
||||
"enable_discount_accounting",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"frozen_accounts_modifier",
|
||||
"column_break_4",
|
||||
"report_setting_section",
|
||||
"use_custom_cash_flow",
|
||||
"deferred_accounting_settings_section",
|
||||
"book_deferred_entries_based_on",
|
||||
"column_break_18",
|
||||
"automatically_process_deferred_accounting_entry",
|
||||
"book_deferred_entries_via_journal_entry",
|
||||
"submit_journal_entries",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"column_break_12",
|
||||
@ -43,8 +38,25 @@
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"stale_days",
|
||||
"report_settings_sb",
|
||||
"use_custom_cash_flow"
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"column_break_11",
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"report_settings_sb"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -70,10 +82,6 @@
|
||||
"label": "Determine Address Tax Category From",
|
||||
"options": "Billing Address\nShipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_controller",
|
||||
"fieldtype": "Link",
|
||||
@ -83,6 +91,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
|
||||
"fieldname": "check_supplier_invoice_uniqueness",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Supplier Invoice Number Uniqueness"
|
||||
@ -168,7 +177,7 @@
|
||||
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||
"fieldname": "use_custom_cash_flow",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Custom Cash Flow Format"
|
||||
"label": "Enable Custom Cash Flow Format"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -241,7 +250,7 @@
|
||||
{
|
||||
"fieldname": "accounts_transactions_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transactions Settings"
|
||||
"label": "Credit Limit Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
@ -272,9 +281,72 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "enable_features_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoice Cancellation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Asset Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoicing_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Credit Limits"
|
||||
},
|
||||
{
|
||||
"fieldname": "assets_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Assets"
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Invoice and Billing"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoicing_features_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoicing Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"fieldname": "report_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Report Setting"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -282,7 +354,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified": "2022-02-04 12:32:36.805652",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@ -309,5 +381,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.trigger('bank_account');
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
||||
frm.trigger("make_reconciliation_tool")
|
||||
@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
bank_account: function (frm) {
|
||||
frappe.db.get_value(
|
||||
"Bank Account",
|
||||
frm.bank_account,
|
||||
frm.doc.bank_account,
|
||||
"account",
|
||||
(r) => {
|
||||
frappe.db.get_value(
|
||||
|
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2018-11-22 22:45:00.370913",
|
||||
"creation": "2022-01-19 01:09:13.297137",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
@ -10,6 +10,9 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"company",
|
||||
"column_break_3",
|
||||
"disabled",
|
||||
"section_break_5",
|
||||
"taxes"
|
||||
],
|
||||
"fields": [
|
||||
@ -36,10 +39,24 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-03-08 19:50:21.416513",
|
||||
"modified": "2022-01-18 21:11:23.105589",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template",
|
||||
@ -82,6 +99,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
@ -167,7 +167,8 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"is_pos": 0,
|
||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||
"update_stock": 0,
|
||||
"invoice_number": row.invoice_number
|
||||
"invoice_number": row.invoice_number,
|
||||
"disable_rounded_total": 1
|
||||
})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
|
@ -172,9 +172,10 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
if self.is_return or self.docstatus != 1:
|
||||
return
|
||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||
for d in self.get('items'):
|
||||
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
|
||||
if is_service_item:
|
||||
@ -186,7 +187,7 @@ class POSInvoice(SalesInvoice):
|
||||
elif d.batch_no:
|
||||
self.validate_pos_reserved_batch_qty(d)
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
if is_negative_stock_allowed(item_code=d.item_code):
|
||||
return
|
||||
|
||||
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
||||
|
@ -586,23 +586,29 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
item_price.insert()
|
||||
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
|
||||
pr.save()
|
||||
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||
# rate shouldn't change
|
||||
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||
|
||||
pos_inv.ignore_pricing_rule = 1
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||
# rate should change since pricing rules are ignored
|
||||
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||
try:
|
||||
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||
# rate shouldn't change
|
||||
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||
|
||||
item_price.delete()
|
||||
pos_inv.delete()
|
||||
pr.delete()
|
||||
pos_inv.ignore_pricing_rule = 1
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||
# rate should reset since pricing rules are ignored
|
||||
self.assertEquals(pos_inv.items[0].rate, 450)
|
||||
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||
|
||||
finally:
|
||||
item_price.delete()
|
||||
pos_inv.delete()
|
||||
pr.delete()
|
||||
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
|
@ -84,12 +84,20 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.save()
|
||||
self.write_off_fractional_amount(sales_invoice, data)
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
|
||||
def write_off_fractional_amount(self, invoice, data):
|
||||
pos_invoice_grand_total = sum(d.grand_total for d in data)
|
||||
|
||||
if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
|
||||
invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
|
||||
invoice.save()
|
||||
|
||||
def process_merging_into_credit_note(self, data):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
@ -102,6 +110,7 @@ class POSInvoiceMergeLog(Document):
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
self.write_off_fractional_amount(credit_note, data)
|
||||
credit_note.submit()
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
@ -135,9 +144,15 @@ class POSInvoiceMergeLog(Document):
|
||||
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
|
||||
found = True
|
||||
i.qty = i.qty + item.qty
|
||||
i.amount = i.amount + item.net_amount
|
||||
i.net_amount = i.amount
|
||||
i.base_amount = i.base_amount + item.base_net_amount
|
||||
i.base_net_amount = i.base_amount
|
||||
|
||||
if not found:
|
||||
item.rate = item.net_rate
|
||||
item.amount = item.net_amount
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
items.append(si_item)
|
||||
@ -169,6 +184,7 @@ class POSInvoiceMergeLog(Document):
|
||||
found = True
|
||||
if not found:
|
||||
payments.append(payment)
|
||||
|
||||
rounding_adjustment += doc.rounding_adjustment
|
||||
rounded_total += doc.rounded_total
|
||||
base_rounding_adjustment += doc.base_rounding_adjustment
|
||||
|
@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
@ -150,3 +151,132 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
|
||||
def test_consolidation_round_off_error_1(self):
|
||||
'''
|
||||
Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
|
||||
'''
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
make_stock_entry(
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
item_code="_Test Item",
|
||||
rate=8000,
|
||||
qty=10,
|
||||
)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
|
||||
inv2.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 0)
|
||||
self.assertEqual(consolidated_invoice.status, 'Paid')
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_consolidation_round_off_error_2(self):
|
||||
'''
|
||||
Test the same case as above but with an Unpaid POS Invoice
|
||||
'''
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
make_stock_entry(
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
item_code="_Test Item",
|
||||
rate=8000,
|
||||
qty=10,
|
||||
)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
|
||||
inv2.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
|
||||
inv3.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
|
||||
})
|
||||
inv3.insert()
|
||||
inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertNotEqual(consolidated_invoice.status, 'Paid')
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
@ -249,13 +249,17 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
"free_item_data": [],
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get('child_docname')
|
||||
"child_docname": args.get('child_docname'),
|
||||
})
|
||||
|
||||
if args.ignore_pricing_rule or not args.item_code:
|
||||
if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
|
||||
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
|
||||
item_details, args.get('item_code'))
|
||||
item_details = remove_pricing_rule_for_item(
|
||||
args.get("pricing_rules"),
|
||||
item_details,
|
||||
item_code=args.get("item_code"),
|
||||
rate=args.get("price_list_rate"),
|
||||
)
|
||||
return item_details
|
||||
|
||||
update_args_for_pricing_rule(args)
|
||||
@ -308,8 +312,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
if not doc: return item_details
|
||||
|
||||
elif args.get("pricing_rules"):
|
||||
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
|
||||
item_details, args.get('item_code'))
|
||||
item_details = remove_pricing_rule_for_item(
|
||||
args.get("pricing_rules"),
|
||||
item_details,
|
||||
item_code=args.get("item_code"),
|
||||
rate=args.get("price_list_rate"),
|
||||
)
|
||||
|
||||
return item_details
|
||||
|
||||
@ -390,7 +398,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
item_details[field] += (pricing_rule.get(field, 0)
|
||||
if pricing_rule else args.get(field, 0))
|
||||
|
||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
get_pricing_rule_items,
|
||||
@ -403,6 +411,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||
if pricing_rule.rate_or_discount == 'Discount Percentage':
|
||||
item_details.discount_percentage = 0.0
|
||||
item_details.discount_amount = 0.0
|
||||
item_details.rate = rate or 0.0
|
||||
|
||||
if pricing_rule.rate_or_discount == 'Discount Amount':
|
||||
item_details.discount_amount = 0.0
|
||||
@ -421,6 +430,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||
item_details.applied_on_items = ','.join(items)
|
||||
|
||||
item_details.pricing_rules = ''
|
||||
item_details.pricing_rule_removed = True
|
||||
|
||||
return item_details
|
||||
|
||||
@ -432,9 +442,12 @@ def remove_pricing_rules(item_list):
|
||||
out = []
|
||||
for item in item_list:
|
||||
item = frappe._dict(item)
|
||||
if item.get('pricing_rules'):
|
||||
out.append(remove_pricing_rule_for_item(item.get("pricing_rules"),
|
||||
item, item.item_code))
|
||||
if item.get("pricing_rules"):
|
||||
out.append(
|
||||
remove_pricing_rule_for_item(
|
||||
item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate")
|
||||
)
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
@ -628,6 +628,46 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_remove_pricing_rule(self):
|
||||
item = make_item("Water Flask")
|
||||
make_item_price("Water Flask", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Water Flask Rule",
|
||||
"apply_on": "Item Code",
|
||||
"price_or_product_discount": "Price",
|
||||
"items": [{
|
||||
"item_code": "Water Flask",
|
||||
}],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"company": "_Test Company"
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||
self.assertEqual(si.items[0].discount_percentage, 20)
|
||||
self.assertEqual(si.items[0].rate, 80)
|
||||
|
||||
si.ignore_pricing_rule = 1
|
||||
si.save()
|
||||
|
||||
self.assertEqual(si.items[0].discount_percentage, 0)
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
|
||||
item.delete()
|
||||
|
||||
def test_multiple_pricing_rules_with_min_qty(self):
|
||||
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
|
||||
@ -648,6 +688,7 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
def make_pricing_rule(**args):
|
||||
|
@ -178,8 +178,8 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||
_("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
|
||||
)
|
||||
|
||||
self.party_account_currency = account.account_currency
|
||||
@ -537,8 +537,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
voucher_wise_stock_value = {}
|
||||
if self.update_stock:
|
||||
for d in frappe.get_all('Stock Ledger Entry',
|
||||
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
|
||||
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
|
||||
filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
|
||||
)
|
||||
for d in stock_ledger_entries:
|
||||
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
|
||||
|
||||
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
|
||||
|
@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = {
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -285,7 +285,7 @@ class SalesInvoice(SellingController):
|
||||
filters={ invoice_or_credit_note: self.name },
|
||||
pluck="pos_closing_entry"
|
||||
)
|
||||
if pos_closing_entry:
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
|
||||
@ -572,7 +572,10 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
|
||||
msg = _("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"),
|
||||
frappe.bold(self.debit_to)
|
||||
) + " "
|
||||
msg += _("Change the account type to Receivable or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
@ -1249,14 +1252,14 @@ class SalesInvoice(SellingController):
|
||||
def update_billing_status_in_dn(self, update_modified=True):
|
||||
updated_delivery_notes = []
|
||||
for d in self.get("items"):
|
||||
if d.dn_detail:
|
||||
if d.so_detail:
|
||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||
elif d.dn_detail:
|
||||
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
|
||||
where dn_detail=%s and docstatus=1""", d.dn_detail)
|
||||
billed_amt = billed_amt and billed_amt[0][0] or 0
|
||||
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
|
||||
updated_delivery_notes.append(d.delivery_note)
|
||||
elif d.so_detail:
|
||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||
|
||||
for dn in set(updated_delivery_notes):
|
||||
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
|
||||
|
@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = {
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
||||
},
|
||||
right_column: "grand_total"
|
||||
right_column: "grand_total",
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Delivery Note"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -2,12 +2,13 @@
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2018-11-22 23:38:39.668804",
|
||||
"creation": "2022-01-19 01:09:28.920486",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title"
|
||||
"title",
|
||||
"disabled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -18,14 +19,21 @@
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-03 11:50:38.748872",
|
||||
"modified": "2022-01-18 21:13:41.161017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Category",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -65,5 +73,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -319,13 +319,18 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
|
||||
"""
|
||||
|
||||
if not gl_entries:
|
||||
gl_entries = frappe.get_all("GL Entry",
|
||||
fields = ["*"],
|
||||
filters = {
|
||||
"voucher_type": voucher_type,
|
||||
"voucher_no": voucher_no,
|
||||
"is_cancelled": 0
|
||||
})
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (frappe.qb.from_(
|
||||
gl_entry
|
||||
).select(
|
||||
'*'
|
||||
).where(
|
||||
gl_entry.voucher_type == voucher_type
|
||||
).where(
|
||||
gl_entry.voucher_no == voucher_no
|
||||
).where(
|
||||
gl_entry.is_cancelled == 0
|
||||
).for_update()).run(as_dict=1)
|
||||
|
||||
if gl_entries:
|
||||
validate_accounting_period(gl_entries)
|
||||
@ -333,23 +338,24 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
|
||||
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
|
||||
|
||||
for entry in gl_entries:
|
||||
entry['name'] = None
|
||||
debit = entry.get('debit', 0)
|
||||
credit = entry.get('credit', 0)
|
||||
new_gle = copy.deepcopy(entry)
|
||||
new_gle['name'] = None
|
||||
debit = new_gle.get('debit', 0)
|
||||
credit = new_gle.get('credit', 0)
|
||||
|
||||
debit_in_account_currency = entry.get('debit_in_account_currency', 0)
|
||||
credit_in_account_currency = entry.get('credit_in_account_currency', 0)
|
||||
debit_in_account_currency = new_gle.get('debit_in_account_currency', 0)
|
||||
credit_in_account_currency = new_gle.get('credit_in_account_currency', 0)
|
||||
|
||||
entry['debit'] = credit
|
||||
entry['credit'] = debit
|
||||
entry['debit_in_account_currency'] = credit_in_account_currency
|
||||
entry['credit_in_account_currency'] = debit_in_account_currency
|
||||
new_gle['debit'] = credit
|
||||
new_gle['credit'] = debit
|
||||
new_gle['debit_in_account_currency'] = credit_in_account_currency
|
||||
new_gle['credit_in_account_currency'] = debit_in_account_currency
|
||||
|
||||
entry['remarks'] = "On cancellation of " + entry['voucher_no']
|
||||
entry['is_cancelled'] = 1
|
||||
new_gle['remarks'] = "On cancellation of " + new_gle['voucher_no']
|
||||
new_gle['is_cancelled'] = 1
|
||||
|
||||
if entry['debit'] or entry['credit']:
|
||||
make_entry(entry, adv_adj, "Yes")
|
||||
if new_gle['debit'] or new_gle['credit']:
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
|
||||
def check_freezing_date(posting_date, adv_adj=False):
|
||||
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2017-08-08 12:33:04.773099",
|
||||
"custom_format": 1,
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\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<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" 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.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"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{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\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 {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\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: 75%\">\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: 75%\">\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: 75%\">\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{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-04-29 16:39:12.936215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST POS Invoice",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity):
|
||||
opening_balance = 0
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if asset:
|
||||
opening_balance = flt(asset[0].get("opening_balance", 0), float_precision)
|
||||
opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision)
|
||||
if liability:
|
||||
opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision)
|
||||
opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision)
|
||||
if equity:
|
||||
opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision)
|
||||
opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision)
|
||||
|
||||
opening_balance = flt(opening_balance, float_precision)
|
||||
if opening_balance:
|
||||
|
@ -282,7 +282,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
total_row = {
|
||||
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"currency": company_currency
|
||||
"currency": company_currency,
|
||||
"opening_balance": 0.0
|
||||
}
|
||||
|
||||
for row in out:
|
||||
@ -294,6 +295,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
|
||||
total_row.setdefault("total", 0.0)
|
||||
total_row["total"] += flt(row["total"])
|
||||
total_row["opening_balance"] += row["opening_balance"]
|
||||
row["total"] = ""
|
||||
|
||||
if "total" in total_row:
|
||||
|
@ -42,6 +42,11 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"parent_field": "parent_invoice",
|
||||
"initial_depth": 3,
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && (data.indent == 0.0 || row[1].content == "Total")) {
|
||||
|
@ -23,7 +23,7 @@ def validate_filters(filters):
|
||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
supplier_map = get_supplier_pan_map()
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(filters, tds_docs)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
@ -78,7 +78,7 @@ def get_supplier_pan_map():
|
||||
|
||||
return supplier_map
|
||||
|
||||
def get_gle_map(filters, documents):
|
||||
def get_gle_map(documents):
|
||||
# create gle_map of the form
|
||||
# {"purchase_invoice": list of dict of all gle created for this invoice}
|
||||
gle_map = {}
|
||||
@ -86,7 +86,7 @@ def get_gle_map(filters, documents):
|
||||
gle = frappe.db.get_all('GL Entry',
|
||||
{
|
||||
"voucher_no": ["in", documents],
|
||||
"credit": (">", 0)
|
||||
"is_cancelled": 0
|
||||
},
|
||||
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
|
||||
)
|
||||
@ -184,21 +184,28 @@ def get_tds_docs(filters):
|
||||
payment_entries = []
|
||||
journal_entries = []
|
||||
tax_category_map = {}
|
||||
or_filters = {}
|
||||
bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name")
|
||||
|
||||
tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
|
||||
pluck="account")
|
||||
|
||||
query_filters = {
|
||||
"credit": ('>', 0),
|
||||
"account": ("in", tds_accounts),
|
||||
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||
"is_cancelled": 0
|
||||
"is_cancelled": 0,
|
||||
"against": ("not in", bank_accounts)
|
||||
}
|
||||
|
||||
if filters.get('supplier'):
|
||||
query_filters.update({'against': filters.get('supplier')})
|
||||
if filters.get("supplier"):
|
||||
del query_filters["account"]
|
||||
del query_filters["against"]
|
||||
or_filters = {
|
||||
"against": filters.get('supplier'),
|
||||
"party": filters.get('supplier')
|
||||
}
|
||||
|
||||
tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"])
|
||||
tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"])
|
||||
|
||||
for d in tds_docs:
|
||||
if d.voucher_type == "Purchase Invoice":
|
||||
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Bulk Transaction Log', {
|
||||
|
||||
before_load: function(frm) {
|
||||
query(frm);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
|
||||
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
|
||||
query(frm);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function query(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
|
||||
args: {
|
||||
log_date: frm.doc.log_date
|
||||
}
|
||||
}).then((r) => {
|
||||
if (r.message) {
|
||||
frm.remove_custom_button("Retry Failed Transactions");
|
||||
} else {
|
||||
frappe.show_alert(__("Retrying Failed Transactions"), 5);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-30 13:41:16.343827",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"log_date",
|
||||
"logger_data"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "log_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Log Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "logger_data",
|
||||
"fieldtype": "Table",
|
||||
"label": "Logger Data",
|
||||
"options": "Bulk Transaction Log Detail"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-03 17:23:02.935325",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.utilities.bulk_transaction import task, update_logger
|
||||
|
||||
|
||||
class BulkTransactionLog(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def retry_failing_transaction(log_date=None):
|
||||
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
|
||||
data = (
|
||||
frappe.qb.from_(btp)
|
||||
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
|
||||
.distinct()
|
||||
.where(btp.retried != 1)
|
||||
.where(btp.transaction_status == "Failed")
|
||||
.where(btp.date == log_date)
|
||||
).run(as_dict=True)
|
||||
|
||||
if data:
|
||||
if not log_date:
|
||||
log_date = str(date.today())
|
||||
if len(data) > 10:
|
||||
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
|
||||
else:
|
||||
job(data, log_date)
|
||||
else:
|
||||
return "No Failed Records"
|
||||
|
||||
def job(data, log_date):
|
||||
for d in data:
|
||||
failed = []
|
||||
try:
|
||||
frappe.db.savepoint("before_creation_of_record")
|
||||
task(d.transaction_name, d.from_doctype, d.to_doctype)
|
||||
except Exception as e:
|
||||
frappe.db.rollback(save_point="before_creation_of_record")
|
||||
failed.append(e)
|
||||
update_logger(
|
||||
d.transaction_name,
|
||||
e,
|
||||
d.from_doctype,
|
||||
d.to_doctype,
|
||||
status="Failed",
|
||||
log_date=log_date,
|
||||
restarted=1
|
||||
)
|
||||
|
||||
if not failed:
|
||||
update_logger(
|
||||
d.transaction_name,
|
||||
None,
|
||||
d.from_doctype,
|
||||
d.to_doctype,
|
||||
status="Success",
|
||||
log_date=log_date,
|
||||
restarted=1,
|
||||
)
|
@ -0,0 +1,81 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.utilities.bulk_transaction import transaction_processing
|
||||
|
||||
|
||||
class TestBulkTransactionLog(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
create_company()
|
||||
create_customer()
|
||||
create_item()
|
||||
|
||||
def test_for_single_record(self):
|
||||
so_name = create_so()
|
||||
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
|
||||
data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"])
|
||||
if not data:
|
||||
self.fail("No Sales Invoice Created !")
|
||||
|
||||
def test_entry_in_log(self):
|
||||
so_name = create_so()
|
||||
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
|
||||
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
|
||||
for d in doc.get("logger_data"):
|
||||
if d.transaction_name == so_name:
|
||||
self.assertEqual(d.transaction_name, so_name)
|
||||
self.assertEqual(d.transaction_status, "Success")
|
||||
self.assertEqual(d.from_doctype, "Sales Order")
|
||||
self.assertEqual(d.to_doctype, "Sales Invoice")
|
||||
self.assertEqual(d.retried, 0)
|
||||
|
||||
|
||||
|
||||
def create_company():
|
||||
if not frappe.db.exists('Company', '_Test Company'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Company',
|
||||
'company_name': '_Test Company',
|
||||
'country': 'India',
|
||||
'default_currency': 'INR'
|
||||
}).insert()
|
||||
|
||||
def create_customer():
|
||||
if not frappe.db.exists('Customer', 'Bulk Customer'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Customer',
|
||||
'customer_name': 'Bulk Customer'
|
||||
}).insert()
|
||||
|
||||
def create_item():
|
||||
if not frappe.db.exists("Item", "MK"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Item",
|
||||
"item_code": "MK",
|
||||
"item_name": "Milk",
|
||||
"description": "Milk",
|
||||
"item_group": "Products"
|
||||
}).insert()
|
||||
|
||||
def create_so(intent=None):
|
||||
so = frappe.new_doc("Sales Order")
|
||||
so.customer = "Bulk Customer"
|
||||
so.company = "_Test Company"
|
||||
so.transaction_date = date.today()
|
||||
|
||||
so.set_warehouse = "Finished Goods - _TC"
|
||||
so.append("items", {
|
||||
"item_code": "MK",
|
||||
"delivery_date": date.today(),
|
||||
"qty": 10,
|
||||
"rate": 80,
|
||||
})
|
||||
so.insert()
|
||||
so.submit()
|
||||
return so.name
|
@ -0,0 +1,86 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-30 13:38:30.926047",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"transaction_name",
|
||||
"date",
|
||||
"time",
|
||||
"transaction_status",
|
||||
"error_description",
|
||||
"from_doctype",
|
||||
"to_doctype",
|
||||
"retried"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "transaction_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"options": "from_doctype"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_status",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "error_description",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Error Description",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Doctype",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "To Doctype",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date ",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "retried",
|
||||
"fieldtype": "Int",
|
||||
"label": "Retried",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-03 19:57:31.650359",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction Log Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BulkTransactionLogDetail(Document):
|
||||
pass
|
@ -6,14 +6,17 @@
|
||||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_and_price_defaults_section",
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"column_break_4",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"transaction_settings_section",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
@ -42,10 +45,6 @@
|
||||
"label": "Default Buying Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
@ -73,7 +72,7 @@
|
||||
{
|
||||
"fieldname": "subcontract",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subcontract"
|
||||
"label": "Subcontracting Settings"
|
||||
},
|
||||
{
|
||||
"default": "Material Transferred for Subcontract",
|
||||
@ -116,6 +115,24 @@
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_and_price_defaults_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier and Price Defaults"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transaction Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -123,7 +140,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-08 19:26:23.548837",
|
||||
"modified": "2022-01-27 17:57:58.367048",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -141,5 +158,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = {
|
||||
listview.call_for_selected_items(method, { "status": "Closed" });
|
||||
});
|
||||
|
||||
listview.page.add_menu_item(__("Re-open"), function () {
|
||||
listview.page.add_menu_item(__("Reopen"), function () {
|
||||
listview.call_for_selected_items(method, { "status": "Submitted" });
|
||||
});
|
||||
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Advance Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
@ -682,17 +682,18 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
bin1 = frappe.db.get_value("Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
|
||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
|
||||
|
||||
# Submit PO
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
|
||||
|
||||
bin2 = frappe.db.get_value("Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
|
||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
|
||||
|
||||
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
||||
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
|
||||
self.assertNotEqual(bin1.modified, bin2.modified)
|
||||
|
||||
# Create stock transfer
|
||||
rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",
|
||||
|
@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None):
|
||||
|
||||
return doclist
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name, target_doc=None):
|
||||
doc = get_mapped_doc("Supplier Quotation", source_name, {
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Invoice",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
}
|
||||
},
|
||||
"Supplier Quotation Item": {
|
||||
"doctype": "Purchase Invoice Item"
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges"
|
||||
}
|
||||
}, target_doc)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quotation(source_name, target_doc=None):
|
||||
doclist = get_mapped_doc("Supplier Quotation", source_name, {
|
||||
|
@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = {
|
||||
} else if(doc.status==="Expired") {
|
||||
return [__("Expired"), "gray", "status,=,Expired"];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Purchase Order"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ valid_scorecard = [
|
||||
"min_grade":0.0,"name":"Very Poor",
|
||||
"prevent_rfqs":1,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":30.0,
|
||||
"prevent_pos":1,
|
||||
"warn_pos":0,
|
||||
@ -65,7 +65,7 @@ valid_scorecard = [
|
||||
"name":"Poor",
|
||||
"prevent_rfqs":1,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":50.0,
|
||||
"prevent_pos":0,
|
||||
"warn_pos":0,
|
||||
@ -81,7 +81,7 @@ valid_scorecard = [
|
||||
"name":"Average",
|
||||
"prevent_rfqs":0,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":80.0,
|
||||
"prevent_pos":0,
|
||||
"warn_pos":0,
|
||||
@ -97,7 +97,7 @@ valid_scorecard = [
|
||||
"name":"Excellent",
|
||||
"prevent_rfqs":0,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":100.0,
|
||||
"prevent_pos":0,
|
||||
"warn_pos":0,
|
||||
|
@ -407,6 +407,22 @@ class AccountsController(TransactionBase):
|
||||
if item_qty != len(get_serial_nos(item.get('serial_no'))):
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif (
|
||||
ret.get("pricing_rule_removed")
|
||||
and value is not None
|
||||
and fieldname
|
||||
in [
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"rate",
|
||||
"margin_rate_or_amount",
|
||||
"margin_type",
|
||||
"remove_free_item",
|
||||
]
|
||||
):
|
||||
# reset pricing rule fields if pricing_rule_removed
|
||||
item.set(fieldname, value)
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
|
||||
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
|
||||
|
||||
@ -1318,6 +1334,9 @@ class AccountsController(TransactionBase):
|
||||
payment_schedule['discount_type'] = schedule.discount_type
|
||||
payment_schedule['discount'] = schedule.discount
|
||||
|
||||
if not schedule.invoice_portion:
|
||||
payment_schedule['payment_amount'] = schedule.payment_amount
|
||||
|
||||
self.append("payment_schedule", payment_schedule)
|
||||
|
||||
def set_due_date(self):
|
||||
|
@ -710,6 +710,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
|
||||
item_group = filters.get('item_group')
|
||||
company = filters.get('company')
|
||||
taxes = item_doc.taxes or []
|
||||
|
||||
while item_group:
|
||||
@ -718,7 +719,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if not taxes:
|
||||
return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
|
||||
return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True)
|
||||
else:
|
||||
valid_from = filters.get('valid_from')
|
||||
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
|
||||
@ -727,7 +728,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
'item_code': filters.get('item_code'),
|
||||
'posting_date': valid_from,
|
||||
'tax_category': filters.get('tax_category'),
|
||||
'company': filters.get('company')
|
||||
'company': company
|
||||
}
|
||||
|
||||
taxes = _get_item_tax_template(args, taxes, for_validate=True)
|
||||
|
@ -74,7 +74,8 @@ class SellingController(StockController):
|
||||
doctype=self.doctype, company=self.company,
|
||||
posting_date=self.get('posting_date'),
|
||||
fetch_payment_terms_template=fetch_payment_terms_template,
|
||||
party_address=self.customer_address, shipping_address=self.shipping_address_name)
|
||||
party_address=self.customer_address, shipping_address=self.shipping_address_name,
|
||||
company_address=self.get('company_address'))
|
||||
if not self.meta.get_field("sales_team"):
|
||||
party_details.pop("sales_team")
|
||||
self.update_if_missing(party_details)
|
||||
|
@ -400,6 +400,16 @@ class StatusUpdater(Document):
|
||||
ref_doc = frappe.get_doc(ref_dt, ref_dn)
|
||||
|
||||
ref_doc.db_set("per_billed", per_billed)
|
||||
|
||||
# set billling status
|
||||
if hasattr(ref_doc, 'billing_status'):
|
||||
if ref_doc.per_billed < 0.001:
|
||||
ref_doc.db_set("billing_status", "Not Billed")
|
||||
elif ref_doc.per_billed > 99.999999:
|
||||
ref_doc.db_set("billing_status", "Fully Billed")
|
||||
else:
|
||||
ref_doc.db_set("billing_status", "Partly Billed")
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@ -181,33 +182,28 @@ class StockController(AccountsController):
|
||||
|
||||
return details
|
||||
|
||||
def get_items_and_warehouses(self):
|
||||
items, warehouses = [], []
|
||||
def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]:
|
||||
"""Get list of items and warehouses affected by a transaction"""
|
||||
|
||||
if hasattr(self, "items"):
|
||||
item_doclist = self.get("items")
|
||||
elif self.doctype == "Stock Reconciliation":
|
||||
item_doclist = []
|
||||
data = json.loads(self.reconciliation_json)
|
||||
for row in data[data.index(self.head_row)+1:]:
|
||||
d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row))
|
||||
item_doclist.append(d)
|
||||
if not (hasattr(self, "items") or hasattr(self, "packed_items")):
|
||||
return [], []
|
||||
|
||||
if item_doclist:
|
||||
for d in item_doclist:
|
||||
if d.item_code and d.item_code not in items:
|
||||
items.append(d.item_code)
|
||||
item_rows = (self.get("items") or []) + (self.get("packed_items") or [])
|
||||
|
||||
if d.get("warehouse") and d.warehouse not in warehouses:
|
||||
warehouses.append(d.warehouse)
|
||||
items = {d.item_code for d in item_rows if d.item_code}
|
||||
|
||||
if self.doctype == "Stock Entry":
|
||||
if d.get("s_warehouse") and d.s_warehouse not in warehouses:
|
||||
warehouses.append(d.s_warehouse)
|
||||
if d.get("t_warehouse") and d.t_warehouse not in warehouses:
|
||||
warehouses.append(d.t_warehouse)
|
||||
warehouses = set()
|
||||
for d in item_rows:
|
||||
if d.get("warehouse"):
|
||||
warehouses.add(d.warehouse)
|
||||
|
||||
return items, warehouses
|
||||
if self.doctype == "Stock Entry":
|
||||
if d.get("s_warehouse"):
|
||||
warehouses.add(d.s_warehouse)
|
||||
if d.get("t_warehouse"):
|
||||
warehouses.add(d.t_warehouse)
|
||||
|
||||
return list(items), list(warehouses)
|
||||
|
||||
def get_stock_ledger_details(self):
|
||||
stock_ledger = {}
|
||||
@ -219,7 +215,7 @@ class StockController(AccountsController):
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
voucher_type=%s and voucher_no=%s
|
||||
voucher_type=%s and voucher_no=%s and is_cancelled = 0
|
||||
""", (self.doctype, self.name), as_dict=True)
|
||||
|
||||
for sle in stock_ledger_entries:
|
||||
|
@ -106,6 +106,9 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.conversion_rate = flt(self.doc.conversion_rate)
|
||||
|
||||
def calculate_item_values(self):
|
||||
if self.doc.get('is_consolidated'):
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
for item in self.doc.get("items"):
|
||||
self.doc.round_floats_in(item)
|
||||
@ -647,12 +650,12 @@ class calculate_taxes_and_totals(object):
|
||||
def calculate_change_amount(self):
|
||||
self.doc.change_amount = 0.0
|
||||
self.doc.base_change_amount = 0.0
|
||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
||||
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
|
||||
|
||||
if self.doc.doctype == "Sales Invoice" \
|
||||
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
|
||||
and self.doc.paid_amount > grand_total and not self.doc.is_return \
|
||||
and any(d.type == "Cash" for d in self.doc.payments):
|
||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
||||
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
|
||||
|
||||
self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
|
||||
self.doc.write_off_amount, self.doc.precision("change_amount"))
|
||||
|
@ -1,524 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import csv
|
||||
import math
|
||||
import time
|
||||
from io import StringIO
|
||||
|
||||
import dateutil
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
|
||||
|
||||
|
||||
#Get and Create Products
|
||||
def get_products_details():
|
||||
products = get_products_instance()
|
||||
reports = get_reports_instance()
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
market_place_list = return_as_list(mws_settings.market_place_id)
|
||||
|
||||
for marketplace in market_place_list:
|
||||
report_id = request_and_fetch_report_id("_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list)
|
||||
|
||||
if report_id:
|
||||
listings_response = reports.get_report(report_id=report_id)
|
||||
|
||||
#Get ASIN Codes
|
||||
string_io = StringIO(frappe.safe_decode(listings_response.original))
|
||||
csv_rows = list(csv.reader(string_io, delimiter='\t'))
|
||||
asin_list = list(set([row[1] for row in csv_rows[1:]]))
|
||||
#break into chunks of 10
|
||||
asin_chunked_list = list(chunks(asin_list, 10))
|
||||
|
||||
#Map ASIN Codes to SKUs
|
||||
sku_asin = [{"asin":row[1],"sku":row[0]} for row in csv_rows[1:]]
|
||||
|
||||
#Fetch Products List from ASIN
|
||||
for asin_list in asin_chunked_list:
|
||||
products_response = call_mws_method(products.get_matching_product,marketplaceid=marketplace,
|
||||
asins=asin_list)
|
||||
|
||||
matching_products_list = products_response.parsed
|
||||
for product in matching_products_list:
|
||||
skus = [row["sku"] for row in sku_asin if row["asin"]==product.ASIN]
|
||||
for sku in skus:
|
||||
create_item_code(product, sku)
|
||||
|
||||
def get_products_instance():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
products = mws.Products(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region = mws_settings.region,
|
||||
domain = mws_settings.domain
|
||||
)
|
||||
|
||||
return products
|
||||
|
||||
def get_reports_instance():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
reports = mws.Reports(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region = mws_settings.region,
|
||||
domain = mws_settings.domain
|
||||
)
|
||||
|
||||
return reports
|
||||
|
||||
#returns list as expected by amazon API
|
||||
def return_as_list(input_value):
|
||||
if isinstance(input_value, list):
|
||||
return input_value
|
||||
else:
|
||||
return [input_value]
|
||||
|
||||
#function to chunk product data
|
||||
def chunks(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
|
||||
def request_and_fetch_report_id(report_type, start_date=None, end_date=None, marketplaceids=None):
|
||||
reports = get_reports_instance()
|
||||
report_response = reports.request_report(report_type=report_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
marketplaceids=marketplaceids)
|
||||
|
||||
report_request_id = report_response.parsed["ReportRequestInfo"]["ReportRequestId"]["value"]
|
||||
generated_report_id = None
|
||||
#poll to get generated report
|
||||
for x in range(1,10):
|
||||
report_request_list_response = reports.get_report_request_list(requestids=[report_request_id])
|
||||
report_status = report_request_list_response.parsed["ReportRequestInfo"]["ReportProcessingStatus"]["value"]
|
||||
|
||||
if report_status == "_SUBMITTED_" or report_status == "_IN_PROGRESS_":
|
||||
#add time delay to wait for amazon to generate report
|
||||
time.sleep(15)
|
||||
continue
|
||||
elif report_status == "_CANCELLED_":
|
||||
break
|
||||
elif report_status == "_DONE_NO_DATA_":
|
||||
break
|
||||
elif report_status == "_DONE_":
|
||||
generated_report_id = report_request_list_response.parsed["ReportRequestInfo"]["GeneratedReportId"]["value"]
|
||||
break
|
||||
return generated_report_id
|
||||
|
||||
def call_mws_method(mws_method, *args, **kwargs):
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
max_retries = mws_settings.max_retry_limit
|
||||
|
||||
for x in range(0, max_retries):
|
||||
try:
|
||||
response = mws_method(*args, **kwargs)
|
||||
return response
|
||||
except Exception as e:
|
||||
delay = math.pow(4, x) * 125
|
||||
frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
|
||||
time.sleep(delay)
|
||||
continue
|
||||
|
||||
mws_settings.enable_sync = 0
|
||||
mws_settings.save()
|
||||
|
||||
frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded"))
|
||||
|
||||
def create_item_code(amazon_item_json, sku):
|
||||
if frappe.db.get_value("Item", sku):
|
||||
return
|
||||
|
||||
item = frappe.new_doc("Item")
|
||||
|
||||
new_manufacturer = create_manufacturer(amazon_item_json)
|
||||
new_brand = create_brand(amazon_item_json)
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
|
||||
item.item_code = sku
|
||||
item.amazon_item_code = amazon_item_json.ASIN
|
||||
item.item_group = mws_settings.item_group
|
||||
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
|
||||
item.brand = new_brand
|
||||
item.manufacturer = new_manufacturer
|
||||
|
||||
item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
|
||||
|
||||
temp_item_group = amazon_item_json.Product.AttributeSets.ItemAttributes.ProductGroup
|
||||
|
||||
item_group = frappe.db.get_value("Item Group",filters={"item_group_name": temp_item_group})
|
||||
|
||||
if not item_group:
|
||||
igroup = frappe.new_doc("Item Group")
|
||||
igroup.item_group_name = temp_item_group
|
||||
igroup.parent_item_group = mws_settings.item_group
|
||||
igroup.insert()
|
||||
|
||||
item.append("item_defaults", {'company':mws_settings.company})
|
||||
|
||||
item.insert(ignore_permissions=True)
|
||||
create_item_price(amazon_item_json, item.item_code)
|
||||
|
||||
return item.name
|
||||
|
||||
def create_manufacturer(amazon_item_json):
|
||||
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer:
|
||||
return None
|
||||
|
||||
existing_manufacturer = frappe.db.get_value("Manufacturer",
|
||||
filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer})
|
||||
|
||||
if not existing_manufacturer:
|
||||
manufacturer = frappe.new_doc("Manufacturer")
|
||||
manufacturer.short_name = amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer
|
||||
manufacturer.insert()
|
||||
return manufacturer.short_name
|
||||
else:
|
||||
return existing_manufacturer
|
||||
|
||||
def create_brand(amazon_item_json):
|
||||
if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand:
|
||||
return None
|
||||
|
||||
existing_brand = frappe.db.get_value("Brand",
|
||||
filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand})
|
||||
if not existing_brand:
|
||||
brand = frappe.new_doc("Brand")
|
||||
brand.brand = amazon_item_json.Product.AttributeSets.ItemAttributes.Brand
|
||||
brand.insert()
|
||||
return brand.brand
|
||||
else:
|
||||
return existing_brand
|
||||
|
||||
def create_item_price(amazon_item_json, item_code):
|
||||
item_price = frappe.new_doc("Item Price")
|
||||
item_price.price_list = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "price_list")
|
||||
if not("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes):
|
||||
item_price.price_list_rate = 0
|
||||
else:
|
||||
item_price.price_list_rate = amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount
|
||||
|
||||
item_price.item_code = item_code
|
||||
item_price.insert()
|
||||
|
||||
#Get and create Orders
|
||||
def get_orders(after_date):
|
||||
try:
|
||||
orders = get_orders_instance()
|
||||
statuses = ["PartiallyShipped", "Unshipped", "Shipped", "Canceled"]
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
market_place_list = return_as_list(mws_settings.market_place_id)
|
||||
|
||||
orders_response = call_mws_method(orders.list_orders, marketplaceids=market_place_list,
|
||||
fulfillment_channels=["MFN", "AFN"],
|
||||
lastupdatedafter=after_date,
|
||||
orderstatus=statuses,
|
||||
max_results='50')
|
||||
|
||||
while True:
|
||||
orders_list = []
|
||||
|
||||
if "Order" in orders_response.parsed.Orders:
|
||||
orders_list = return_as_list(orders_response.parsed.Orders.Order)
|
||||
|
||||
if len(orders_list) == 0:
|
||||
break
|
||||
|
||||
for order in orders_list:
|
||||
create_sales_order(order, after_date)
|
||||
|
||||
if not "NextToken" in orders_response.parsed:
|
||||
break
|
||||
|
||||
next_token = orders_response.parsed.NextToken
|
||||
orders_response = call_mws_method(orders.list_orders_by_next_token, next_token)
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(title="get_orders", message=e)
|
||||
|
||||
def get_orders_instance():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
orders = mws.Orders(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region= mws_settings.region,
|
||||
domain= mws_settings.domain,
|
||||
version="2013-09-01"
|
||||
)
|
||||
|
||||
return orders
|
||||
|
||||
def create_sales_order(order_json,after_date):
|
||||
customer_name = create_customer(order_json)
|
||||
create_address(order_json, customer_name)
|
||||
|
||||
market_place_order_id = order_json.AmazonOrderId
|
||||
|
||||
so = frappe.db.get_value("Sales Order",
|
||||
filters={"amazon_order_id": market_place_order_id},
|
||||
fieldname="name")
|
||||
|
||||
taxes_and_charges = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges")
|
||||
|
||||
if so:
|
||||
return
|
||||
|
||||
if not so:
|
||||
items = get_order_items(market_place_order_id)
|
||||
delivery_date = dateutil.parser.parse(order_json.LatestShipDate).strftime("%Y-%m-%d")
|
||||
transaction_date = dateutil.parser.parse(order_json.PurchaseDate).strftime("%Y-%m-%d")
|
||||
|
||||
so = frappe.get_doc({
|
||||
"doctype": "Sales Order",
|
||||
"naming_series": "SO-",
|
||||
"amazon_order_id": market_place_order_id,
|
||||
"marketplace_id": order_json.MarketplaceId,
|
||||
"customer": customer_name,
|
||||
"delivery_date": delivery_date,
|
||||
"transaction_date": transaction_date,
|
||||
"items": items,
|
||||
"company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company")
|
||||
})
|
||||
|
||||
try:
|
||||
if taxes_and_charges:
|
||||
charges_and_fees = get_charges_and_fees(market_place_order_id)
|
||||
for charge in charges_and_fees.get("charges"):
|
||||
so.append('taxes', charge)
|
||||
|
||||
for fee in charges_and_fees.get("fees"):
|
||||
so.append('taxes', fee)
|
||||
|
||||
so.insert(ignore_permissions=True)
|
||||
so.submit()
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
frappe.log_error(message=traceback.format_exc(), title="Create Sales Order")
|
||||
|
||||
def create_customer(order_json):
|
||||
order_customer_name = ""
|
||||
|
||||
if not("BuyerName" in order_json):
|
||||
order_customer_name = "Buyer - " + order_json.AmazonOrderId
|
||||
else:
|
||||
order_customer_name = order_json.BuyerName
|
||||
|
||||
existing_customer_name = frappe.db.get_value("Customer",
|
||||
filters={"name": order_customer_name}, fieldname="name")
|
||||
|
||||
if existing_customer_name:
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Customer"],
|
||||
["Dynamic Link", "link_name", "=", existing_customer_name],
|
||||
["Dynamic Link", "parenttype", "=", "Contact"]
|
||||
]
|
||||
|
||||
existing_contacts = frappe.get_list("Contact", filters)
|
||||
|
||||
if existing_contacts:
|
||||
pass
|
||||
else:
|
||||
new_contact = frappe.new_doc("Contact")
|
||||
new_contact.first_name = order_customer_name
|
||||
new_contact.append('links', {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": existing_customer_name
|
||||
})
|
||||
new_contact.insert()
|
||||
|
||||
return existing_customer_name
|
||||
else:
|
||||
mws_customer_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
new_customer = frappe.new_doc("Customer")
|
||||
new_customer.customer_name = order_customer_name
|
||||
new_customer.customer_group = mws_customer_settings.customer_group
|
||||
new_customer.territory = mws_customer_settings.territory
|
||||
new_customer.customer_type = mws_customer_settings.customer_type
|
||||
new_customer.save()
|
||||
|
||||
new_contact = frappe.new_doc("Contact")
|
||||
new_contact.first_name = order_customer_name
|
||||
new_contact.append('links', {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": new_customer.name
|
||||
})
|
||||
|
||||
new_contact.insert()
|
||||
|
||||
return new_customer.name
|
||||
|
||||
def create_address(amazon_order_item_json, customer_name):
|
||||
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Customer"],
|
||||
["Dynamic Link", "link_name", "=", customer_name],
|
||||
["Dynamic Link", "parenttype", "=", "Address"]
|
||||
]
|
||||
|
||||
existing_address = frappe.get_list("Address", filters)
|
||||
|
||||
if not("ShippingAddress" in amazon_order_item_json):
|
||||
return None
|
||||
else:
|
||||
make_address = frappe.new_doc("Address")
|
||||
|
||||
if "AddressLine1" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.address_line1 = amazon_order_item_json.ShippingAddress.AddressLine1
|
||||
else:
|
||||
make_address.address_line1 = "Not Provided"
|
||||
|
||||
if "City" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.city = amazon_order_item_json.ShippingAddress.City
|
||||
else:
|
||||
make_address.city = "Not Provided"
|
||||
|
||||
if "StateOrRegion" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.state = amazon_order_item_json.ShippingAddress.StateOrRegion
|
||||
|
||||
if "PostalCode" in amazon_order_item_json.ShippingAddress:
|
||||
make_address.pincode = amazon_order_item_json.ShippingAddress.PostalCode
|
||||
|
||||
for address in existing_address:
|
||||
address_doc = frappe.get_doc("Address", address["name"])
|
||||
if (address_doc.address_line1 == make_address.address_line1 and
|
||||
address_doc.pincode == make_address.pincode):
|
||||
return address
|
||||
|
||||
make_address.append("links", {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": customer_name
|
||||
})
|
||||
make_address.address_type = "Shipping"
|
||||
make_address.insert()
|
||||
|
||||
def get_order_items(market_place_order_id):
|
||||
mws_orders = get_orders_instance()
|
||||
|
||||
order_items_response = call_mws_method(mws_orders.list_order_items, amazon_order_id=market_place_order_id)
|
||||
final_order_items = []
|
||||
|
||||
order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
|
||||
|
||||
warehouse = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "warehouse")
|
||||
|
||||
while True:
|
||||
for order_item in order_items_list:
|
||||
|
||||
if not "ItemPrice" in order_item:
|
||||
price = 0
|
||||
else:
|
||||
price = order_item.ItemPrice.Amount
|
||||
|
||||
final_order_items.append({
|
||||
"item_code": get_item_code(order_item),
|
||||
"item_name": order_item.SellerSKU,
|
||||
"description": order_item.Title,
|
||||
"rate": price,
|
||||
"qty": order_item.QuantityOrdered,
|
||||
"stock_uom": "Nos",
|
||||
"warehouse": warehouse,
|
||||
"conversion_factor": "1.0"
|
||||
})
|
||||
|
||||
if not "NextToken" in order_items_response.parsed:
|
||||
break
|
||||
|
||||
next_token = order_items_response.parsed.NextToken
|
||||
|
||||
order_items_response = call_mws_method(mws_orders.list_order_items_by_next_token, next_token)
|
||||
order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem)
|
||||
|
||||
return final_order_items
|
||||
|
||||
def get_item_code(order_item):
|
||||
sku = order_item.SellerSKU
|
||||
item_code = frappe.db.get_value("Item", {"item_code": sku}, "item_code")
|
||||
if item_code:
|
||||
return item_code
|
||||
|
||||
def get_charges_and_fees(market_place_order_id):
|
||||
finances = get_finances_instance()
|
||||
|
||||
charges_fees = {"charges":[], "fees":[]}
|
||||
|
||||
response = call_mws_method(finances.list_financial_events, amazon_order_id=market_place_order_id)
|
||||
|
||||
shipment_event_list = return_as_list(response.parsed.FinancialEvents.ShipmentEventList)
|
||||
|
||||
for shipment_event in shipment_event_list:
|
||||
if shipment_event:
|
||||
shipment_item_list = return_as_list(shipment_event.ShipmentEvent.ShipmentItemList.ShipmentItem)
|
||||
|
||||
for shipment_item in shipment_item_list:
|
||||
charges, fees = [], []
|
||||
|
||||
if 'ItemChargeList' in shipment_item.keys():
|
||||
charges = return_as_list(shipment_item.ItemChargeList.ChargeComponent)
|
||||
|
||||
if 'ItemFeeList' in shipment_item.keys():
|
||||
fees = return_as_list(shipment_item.ItemFeeList.FeeComponent)
|
||||
|
||||
for charge in charges:
|
||||
if(charge.ChargeType != "Principal") and float(charge.ChargeAmount.CurrencyAmount) != 0:
|
||||
charge_account = get_account(charge.ChargeType)
|
||||
charges_fees.get("charges").append({
|
||||
"charge_type":"Actual",
|
||||
"account_head": charge_account,
|
||||
"tax_amount": charge.ChargeAmount.CurrencyAmount,
|
||||
"description": charge.ChargeType + " for " + shipment_item.SellerSKU
|
||||
})
|
||||
|
||||
for fee in fees:
|
||||
if float(fee.FeeAmount.CurrencyAmount) != 0:
|
||||
fee_account = get_account(fee.FeeType)
|
||||
charges_fees.get("fees").append({
|
||||
"charge_type":"Actual",
|
||||
"account_head": fee_account,
|
||||
"tax_amount": fee.FeeAmount.CurrencyAmount,
|
||||
"description": fee.FeeType + " for " + shipment_item.SellerSKU
|
||||
})
|
||||
|
||||
return charges_fees
|
||||
|
||||
def get_finances_instance():
|
||||
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
|
||||
finances = mws.Finances(
|
||||
account_id = mws_settings.seller_id,
|
||||
access_key = mws_settings.aws_access_key_id,
|
||||
secret_key = mws_settings.secret_key,
|
||||
region= mws_settings.region,
|
||||
domain= mws_settings.domain,
|
||||
version="2015-05-01"
|
||||
)
|
||||
|
||||
return finances
|
||||
|
||||
def get_account(name):
|
||||
existing_account = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)})
|
||||
account_name = existing_account
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
|
||||
if not existing_account:
|
||||
try:
|
||||
new_account = frappe.new_doc("Account")
|
||||
new_account.account_name = "Amazon {0}".format(name)
|
||||
new_account.company = mws_settings.company
|
||||
new_account.parent_account = mws_settings.market_place_account_group
|
||||
new_account.insert(ignore_permissions=True)
|
||||
account_name = new_account.name
|
||||
except Exception as e:
|
||||
frappe.log_error(message=e, title="Create Account")
|
||||
|
||||
return account_name
|
@ -1,651 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Basic interface to Amazon MWS
|
||||
# Based on http://code.google.com/p/amazon-mws-python
|
||||
# Extended to include finances object
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from erpnext.erpnext_integrations.doctype.amazon_mws_settings import xml_utils
|
||||
|
||||
try:
|
||||
from xml.etree.ElementTree import ParseError as XMLError
|
||||
except ImportError:
|
||||
from xml.parsers.expat import ExpatError as XMLError
|
||||
|
||||
from time import gmtime, strftime
|
||||
|
||||
from requests import request
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
__all__ = [
|
||||
'Feeds',
|
||||
'Inventory',
|
||||
'MWSError',
|
||||
'Reports',
|
||||
'Orders',
|
||||
'Products',
|
||||
'Recommendations',
|
||||
'Sellers',
|
||||
'Finances'
|
||||
]
|
||||
|
||||
# See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf page 8
|
||||
# for a list of the end points and marketplace IDs
|
||||
|
||||
MARKETPLACES = {
|
||||
"CA": "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2
|
||||
"US": "https://mws.amazonservices.com", #ATVPDKIKX0DER",
|
||||
"DE": "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9
|
||||
"ES": "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS
|
||||
"FR": "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH
|
||||
"IN": "https://mws.amazonservices.in", #A21TJRUUN4KGV
|
||||
"IT": "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4
|
||||
"UK": "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P
|
||||
"JP": "https://mws.amazonservices.jp", #A1VC38T7YXB528
|
||||
"CN": "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW
|
||||
"AE": " https://mws.amazonservices.ae", #A2VIGQ35RCS4UG
|
||||
"MX": "https://mws.amazonservices.com.mx", #A1AM78C64UM0Y8
|
||||
"BR": "https://mws.amazonservices.com", #A2Q3Y263D00KWC
|
||||
}
|
||||
|
||||
|
||||
class MWSError(Exception):
|
||||
"""
|
||||
Main MWS Exception class
|
||||
"""
|
||||
# Allows quick access to the response object.
|
||||
# Do not rely on this attribute, always check if its not None.
|
||||
response = None
|
||||
|
||||
def calc_md5(string):
|
||||
"""Calculates the MD5 encryption for the given string
|
||||
"""
|
||||
md = hashlib.md5()
|
||||
md.update(string)
|
||||
return base64.encodebytes(md.digest()).decode().strip()
|
||||
|
||||
|
||||
|
||||
def remove_empty(d):
|
||||
"""
|
||||
Helper function that removes all keys from a dictionary (d),
|
||||
that have an empty value.
|
||||
"""
|
||||
for key in list(d):
|
||||
if not d[key]:
|
||||
del d[key]
|
||||
return d
|
||||
|
||||
def remove_namespace(xml):
|
||||
xml = xml.decode('utf-8')
|
||||
regex = re.compile(' xmlns(:ns2)?="[^"]+"|(ns2:)|(xml:)')
|
||||
return regex.sub('', xml)
|
||||
|
||||
class DictWrapper(object):
|
||||
def __init__(self, xml, rootkey=None):
|
||||
self.original = xml
|
||||
self._rootkey = rootkey
|
||||
self._mydict = xml_utils.xml2dict().fromstring(remove_namespace(xml))
|
||||
self._response_dict = self._mydict.get(list(self._mydict)[0], self._mydict)
|
||||
|
||||
@property
|
||||
def parsed(self):
|
||||
if self._rootkey:
|
||||
return self._response_dict.get(self._rootkey)
|
||||
else:
|
||||
return self._response_dict
|
||||
|
||||
class DataWrapper(object):
|
||||
"""
|
||||
Text wrapper in charge of validating the hash sent by Amazon.
|
||||
"""
|
||||
def __init__(self, data, header):
|
||||
self.original = data
|
||||
if 'content-md5' in header:
|
||||
hash_ = calc_md5(self.original)
|
||||
if header['content-md5'] != hash_:
|
||||
raise MWSError("Wrong Contentlength, maybe amazon error...")
|
||||
|
||||
@property
|
||||
def parsed(self):
|
||||
return self.original
|
||||
|
||||
class MWS(object):
|
||||
""" Base Amazon API class """
|
||||
|
||||
# This is used to post/get to the different uris used by amazon per api
|
||||
# ie. /Orders/2011-01-01
|
||||
# All subclasses must define their own URI only if needed
|
||||
URI = "/"
|
||||
|
||||
# The API version varies in most amazon APIs
|
||||
VERSION = "2009-01-01"
|
||||
|
||||
# There seem to be some xml namespace issues. therefore every api subclass
|
||||
# is recommended to define its namespace, so that it can be referenced
|
||||
# like so AmazonAPISubclass.NS.
|
||||
# For more information see http://stackoverflow.com/a/8719461/389453
|
||||
NS = ''
|
||||
|
||||
# Some APIs are available only to either a "Merchant" or "Seller"
|
||||
# the type of account needs to be sent in every call to the amazon MWS.
|
||||
# This constant defines the exact name of the parameter Amazon expects
|
||||
# for the specific API being used.
|
||||
# All subclasses need to define this if they require another account type
|
||||
# like "Merchant" in which case you define it like so.
|
||||
# ACCOUNT_TYPE = "Merchant"
|
||||
# Which is the name of the parameter for that specific account type.
|
||||
ACCOUNT_TYPE = "SellerId"
|
||||
|
||||
def __init__(self, access_key, secret_key, account_id, region='US', domain='', uri="", version=""):
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
self.account_id = account_id
|
||||
self.version = version or self.VERSION
|
||||
self.uri = uri or self.URI
|
||||
|
||||
if domain:
|
||||
self.domain = domain
|
||||
elif region in MARKETPLACES:
|
||||
self.domain = MARKETPLACES[region]
|
||||
else:
|
||||
error_msg = "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" % {
|
||||
"marketplaces" : ', '.join(MARKETPLACES.keys()),
|
||||
"region" : region,
|
||||
}
|
||||
raise MWSError(error_msg)
|
||||
|
||||
def make_request(self, extra_data, method="GET", **kwargs):
|
||||
"""Make request to Amazon MWS API with these parameters
|
||||
"""
|
||||
|
||||
# Remove all keys with an empty value because
|
||||
# Amazon's MWS does not allow such a thing.
|
||||
extra_data = remove_empty(extra_data)
|
||||
|
||||
params = {
|
||||
'AWSAccessKeyId': self.access_key,
|
||||
self.ACCOUNT_TYPE: self.account_id,
|
||||
'SignatureVersion': '2',
|
||||
'Timestamp': self.get_timestamp(),
|
||||
'Version': self.version,
|
||||
'SignatureMethod': 'HmacSHA256',
|
||||
}
|
||||
params.update(extra_data)
|
||||
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
|
||||
signature = self.calc_signature(method, request_description)
|
||||
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
|
||||
headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'}
|
||||
headers.update(kwargs.get('extra_headers', {}))
|
||||
|
||||
try:
|
||||
# Some might wonder as to why i don't pass the params dict as the params argument to request.
|
||||
# My answer is, here i have to get the url parsed string of params in order to sign it, so
|
||||
# if i pass the params dict as params to request, request will repeat that step because it will need
|
||||
# to convert the dict to a url parsed string, so why do it twice if i can just pass the full url :).
|
||||
response = request(method, url, data=kwargs.get('body', ''), headers=headers)
|
||||
response.raise_for_status()
|
||||
# When retrieving data from the response object,
|
||||
# be aware that response.content returns the content in bytes while response.text calls
|
||||
# response.content and converts it to unicode.
|
||||
data = response.content
|
||||
|
||||
# I do not check the headers to decide which content structure to server simply because sometimes
|
||||
# Amazon's MWS API returns XML error responses with "text/plain" as the Content-Type.
|
||||
try:
|
||||
parsed_response = DictWrapper(data, extra_data.get("Action") + "Result")
|
||||
except XMLError:
|
||||
parsed_response = DataWrapper(data, response.headers)
|
||||
|
||||
except HTTPError as e:
|
||||
error = MWSError(str(e))
|
||||
error.response = e.response
|
||||
raise error
|
||||
|
||||
# Store the response object in the parsed_response for quick access
|
||||
parsed_response.response = response
|
||||
return parsed_response
|
||||
|
||||
def get_service_status(self):
|
||||
"""
|
||||
Returns a GREEN, GREEN_I, YELLOW or RED status.
|
||||
Depending on the status/availability of the API its being called from.
|
||||
"""
|
||||
|
||||
return self.make_request(extra_data=dict(Action='GetServiceStatus'))
|
||||
|
||||
def calc_signature(self, method, request_description):
|
||||
"""Calculate MWS signature to interface with Amazon
|
||||
"""
|
||||
sig_data = method + '\n' + self.domain.replace('https://', '').lower() + '\n' + self.uri + '\n' + request_description
|
||||
sig_data = sig_data.encode('utf-8')
|
||||
secret_key = self.secret_key.encode('utf-8')
|
||||
digest = hmac.new(secret_key, sig_data, hashlib.sha256).digest()
|
||||
return base64.b64encode(digest).decode('utf-8')
|
||||
|
||||
def get_timestamp(self):
|
||||
"""
|
||||
Returns the current timestamp in proper format.
|
||||
"""
|
||||
return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
|
||||
|
||||
def enumerate_param(self, param, values):
|
||||
"""
|
||||
Builds a dictionary of an enumerated parameter.
|
||||
Takes any iterable and returns a dictionary.
|
||||
ie.
|
||||
enumerate_param('MarketplaceIdList.Id', (123, 345, 4343))
|
||||
returns
|
||||
{
|
||||
MarketplaceIdList.Id.1: 123,
|
||||
MarketplaceIdList.Id.2: 345,
|
||||
MarketplaceIdList.Id.3: 4343
|
||||
}
|
||||
"""
|
||||
params = {}
|
||||
if values is not None:
|
||||
if not param.endswith('.'):
|
||||
param = "%s." % param
|
||||
for num, value in enumerate(values):
|
||||
params['%s%d' % (param, (num + 1))] = value
|
||||
return params
|
||||
|
||||
|
||||
class Feeds(MWS):
|
||||
""" Amazon MWS Feeds API """
|
||||
|
||||
ACCOUNT_TYPE = "Merchant"
|
||||
|
||||
def submit_feed(self, feed, feed_type, marketplaceids=None,
|
||||
content_type="text/xml", purge='false'):
|
||||
"""
|
||||
Uploads a feed ( xml or .tsv ) to the seller's inventory.
|
||||
Can be used for creating/updating products on Amazon.
|
||||
"""
|
||||
data = dict(Action='SubmitFeed',
|
||||
FeedType=feed_type,
|
||||
PurgeAndReplace=purge)
|
||||
data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
|
||||
md = calc_md5(feed)
|
||||
return self.make_request(data, method="POST", body=feed,
|
||||
extra_headers={'Content-MD5': md, 'Content-Type': content_type})
|
||||
|
||||
def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None,
|
||||
processingstatuses=None, fromdate=None, todate=None):
|
||||
"""
|
||||
Returns a list of all feed submissions submitted in the previous 90 days.
|
||||
That match the query parameters.
|
||||
"""
|
||||
|
||||
data = dict(Action='GetFeedSubmissionList',
|
||||
MaxCount=max_count,
|
||||
SubmittedFromDate=fromdate,
|
||||
SubmittedToDate=todate,)
|
||||
data.update(self.enumerate_param('FeedSubmissionIdList.Id', feedids))
|
||||
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
|
||||
data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_submission_list_by_next_token(self, token):
|
||||
data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None):
|
||||
data = dict(Action='GetFeedSubmissionCount',
|
||||
SubmittedFromDate=fromdate,
|
||||
SubmittedToDate=todate)
|
||||
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
|
||||
data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None):
|
||||
data = dict(Action='CancelFeedSubmissions',
|
||||
SubmittedFromDate=fromdate,
|
||||
SubmittedToDate=todate)
|
||||
data.update(self.enumerate_param('FeedSubmissionIdList.Id.', feedids))
|
||||
data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_feed_submission_result(self, feedid):
|
||||
data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid)
|
||||
return self.make_request(data)
|
||||
|
||||
class Reports(MWS):
|
||||
""" Amazon MWS Reports API """
|
||||
|
||||
ACCOUNT_TYPE = "Merchant"
|
||||
|
||||
## REPORTS ###
|
||||
|
||||
def get_report(self, report_id):
|
||||
data = dict(Action='GetReport', ReportId=report_id)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportCount',
|
||||
Acknowledged=acknowledged,
|
||||
AvailableFromDate=fromdate,
|
||||
AvailableToDate=todate)
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None,
|
||||
fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportList',
|
||||
Acknowledged=acknowledged,
|
||||
AvailableFromDate=fromdate,
|
||||
AvailableToDate=todate,
|
||||
MaxCount=max_count)
|
||||
data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_list_by_next_token(self, token):
|
||||
data = dict(Action='GetReportListByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_request_count(self, report_types=(), processingstatuses=(), fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportRequestCount',
|
||||
RequestedFromDate=fromdate,
|
||||
RequestedToDate=todate)
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', report_types))
|
||||
data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_request_list(self, requestids=(), types=(), processingstatuses=(),
|
||||
max_count=None, fromdate=None, todate=None):
|
||||
data = dict(Action='GetReportRequestList',
|
||||
MaxCount=max_count,
|
||||
RequestedFromDate=fromdate,
|
||||
RequestedToDate=todate)
|
||||
data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids))
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_request_list_by_next_token(self, token):
|
||||
data = dict(Action='GetReportRequestListByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()):
|
||||
data = dict(Action='RequestReport',
|
||||
ReportType=report_type,
|
||||
StartDate=start_date,
|
||||
EndDate=end_date)
|
||||
data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids))
|
||||
return self.make_request(data)
|
||||
|
||||
### ReportSchedule ###
|
||||
|
||||
def get_report_schedule_list(self, types=()):
|
||||
data = dict(Action='GetReportScheduleList')
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_report_schedule_count(self, types=()):
|
||||
data = dict(Action='GetReportScheduleCount')
|
||||
data.update(self.enumerate_param('ReportTypeList.Type.', types))
|
||||
return self.make_request(data)
|
||||
|
||||
|
||||
class Orders(MWS):
|
||||
""" Amazon Orders API """
|
||||
|
||||
URI = "/Orders/2013-09-01"
|
||||
VERSION = "2013-09-01"
|
||||
NS = '{https://mws.amazonservices.com/Orders/2011-01-01}'
|
||||
|
||||
def list_orders(self, marketplaceids, created_after=None, created_before=None, lastupdatedafter=None,
|
||||
lastupdatedbefore=None, orderstatus=(), fulfillment_channels=(),
|
||||
payment_methods=(), buyer_email=None, seller_orderid=None, max_results='100'):
|
||||
|
||||
data = dict(Action='ListOrders',
|
||||
CreatedAfter=created_after,
|
||||
CreatedBefore=created_before,
|
||||
LastUpdatedAfter=lastupdatedafter,
|
||||
LastUpdatedBefore=lastupdatedbefore,
|
||||
BuyerEmail=buyer_email,
|
||||
SellerOrderId=seller_orderid,
|
||||
MaxResultsPerPage=max_results,
|
||||
)
|
||||
data.update(self.enumerate_param('OrderStatus.Status.', orderstatus))
|
||||
data.update(self.enumerate_param('MarketplaceId.Id.', marketplaceids))
|
||||
data.update(self.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels))
|
||||
data.update(self.enumerate_param('PaymentMethod.Method.', payment_methods))
|
||||
return self.make_request(data)
|
||||
|
||||
def list_orders_by_next_token(self, token):
|
||||
data = dict(Action='ListOrdersByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_order(self, amazon_order_ids):
|
||||
data = dict(Action='GetOrder')
|
||||
data.update(self.enumerate_param('AmazonOrderId.Id.', amazon_order_ids))
|
||||
return self.make_request(data)
|
||||
|
||||
def list_order_items(self, amazon_order_id):
|
||||
data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id)
|
||||
return self.make_request(data)
|
||||
|
||||
def list_order_items_by_next_token(self, token):
|
||||
data = dict(Action='ListOrderItemsByNextToken', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
|
||||
class Products(MWS):
|
||||
""" Amazon MWS Products API """
|
||||
|
||||
URI = '/Products/2011-10-01'
|
||||
VERSION = '2011-10-01'
|
||||
NS = '{http://mws.amazonservices.com/schema/Products/2011-10-01}'
|
||||
|
||||
def list_matching_products(self, marketplaceid, query, contextid=None):
|
||||
""" Returns a list of products and their attributes, ordered by
|
||||
relevancy, based on a search query that you specify.
|
||||
Your search query can be a phrase that describes the product
|
||||
or it can be a product identifier such as a UPC, EAN, ISBN, or JAN.
|
||||
"""
|
||||
data = dict(Action='ListMatchingProducts',
|
||||
MarketplaceId=marketplaceid,
|
||||
Query=query,
|
||||
QueryContextId=contextid)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_matching_product(self, marketplaceid, asins):
|
||||
""" Returns a list of products and their attributes, based on a list of
|
||||
ASIN values that you specify.
|
||||
"""
|
||||
data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_matching_product_for_id(self, marketplaceid, type, id):
|
||||
""" Returns a list of products and their attributes, based on a list of
|
||||
product identifier values (asin, sellersku, upc, ean, isbn and JAN)
|
||||
Added in Fourth Release, API version 2011-10-01
|
||||
"""
|
||||
data = dict(Action='GetMatchingProductForId',
|
||||
MarketplaceId=marketplaceid,
|
||||
IdType=type)
|
||||
data.update(self.enumerate_param('IdList.Id', id))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_competitive_pricing_for_sku(self, marketplaceid, skus):
|
||||
""" Returns the current competitive pricing of a product,
|
||||
based on the SellerSKU and MarketplaceId that you specify.
|
||||
"""
|
||||
data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid)
|
||||
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_competitive_pricing_for_asin(self, marketplaceid, asins):
|
||||
""" Returns the current competitive pricing of a product,
|
||||
based on the ASIN and MarketplaceId that you specify.
|
||||
"""
|
||||
data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"):
|
||||
data = dict(Action='GetLowestOfferListingsForSKU',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition,
|
||||
ExcludeMe=excludeme)
|
||||
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"):
|
||||
data = dict(Action='GetLowestOfferListingsForASIN',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition,
|
||||
ExcludeMe=excludeme)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_product_categories_for_sku(self, marketplaceid, sku):
|
||||
data = dict(Action='GetProductCategoriesForSKU',
|
||||
MarketplaceId=marketplaceid,
|
||||
SellerSKU=sku)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_product_categories_for_asin(self, marketplaceid, asin):
|
||||
data = dict(Action='GetProductCategoriesForASIN',
|
||||
MarketplaceId=marketplaceid,
|
||||
ASIN=asin)
|
||||
return self.make_request(data)
|
||||
|
||||
def get_my_price_for_sku(self, marketplaceid, skus, condition=None):
|
||||
data = dict(Action='GetMyPriceForSKU',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition)
|
||||
data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus))
|
||||
return self.make_request(data)
|
||||
|
||||
def get_my_price_for_asin(self, marketplaceid, asins, condition=None):
|
||||
data = dict(Action='GetMyPriceForASIN',
|
||||
MarketplaceId=marketplaceid,
|
||||
ItemCondition=condition)
|
||||
data.update(self.enumerate_param('ASINList.ASIN.', asins))
|
||||
return self.make_request(data)
|
||||
|
||||
|
||||
class Sellers(MWS):
|
||||
""" Amazon MWS Sellers API """
|
||||
|
||||
URI = '/Sellers/2011-07-01'
|
||||
VERSION = '2011-07-01'
|
||||
NS = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}'
|
||||
|
||||
def list_marketplace_participations(self):
|
||||
"""
|
||||
Returns a list of marketplaces a seller can participate in and
|
||||
a list of participations that include seller-specific information in that marketplace.
|
||||
The operation returns only those marketplaces where the seller's account is in an active state.
|
||||
"""
|
||||
|
||||
data = dict(Action='ListMarketplaceParticipations')
|
||||
return self.make_request(data)
|
||||
|
||||
def list_marketplace_participations_by_next_token(self, token):
|
||||
"""
|
||||
Takes a "NextToken" and returns the same information as "list_marketplace_participations".
|
||||
Based on the "NextToken".
|
||||
"""
|
||||
data = dict(Action='ListMarketplaceParticipations', NextToken=token)
|
||||
return self.make_request(data)
|
||||
|
||||
#### Fulfillment APIs ####
|
||||
|
||||
class InboundShipments(MWS):
|
||||
URI = "/FulfillmentInboundShipment/2010-10-01"
|
||||
VERSION = '2010-10-01'
|
||||
|
||||
# To be completed
|
||||
|
||||
|
||||
class Inventory(MWS):
|
||||
""" Amazon MWS Inventory Fulfillment API """
|
||||
|
||||
URI = '/FulfillmentInventory/2010-10-01'
|
||||
VERSION = '2010-10-01'
|
||||
NS = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}"
|
||||
|
||||
def list_inventory_supply(self, skus=(), datetime=None, response_group='Basic'):
|
||||
""" Returns information on available inventory """
|
||||
|
||||
data = dict(Action='ListInventorySupply',
|
||||
QueryStartDateTime=datetime,
|
||||
ResponseGroup=response_group,
|
||||
)
|
||||
data.update(self.enumerate_param('SellerSkus.member.', skus))
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
def list_inventory_supply_by_next_token(self, token):
|
||||
data = dict(Action='ListInventorySupplyByNextToken', NextToken=token)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
|
||||
class OutboundShipments(MWS):
|
||||
URI = "/FulfillmentOutboundShipment/2010-10-01"
|
||||
VERSION = "2010-10-01"
|
||||
# To be completed
|
||||
|
||||
|
||||
class Recommendations(MWS):
|
||||
|
||||
""" Amazon MWS Recommendations API """
|
||||
|
||||
URI = '/Recommendations/2013-04-01'
|
||||
VERSION = '2013-04-01'
|
||||
NS = "{https://mws.amazonservices.com/Recommendations/2013-04-01}"
|
||||
|
||||
def get_last_updated_time_for_recommendations(self, marketplaceid):
|
||||
"""
|
||||
Checks whether there are active recommendations for each category for the given marketplace, and if there are,
|
||||
returns the time when recommendations were last updated for each category.
|
||||
"""
|
||||
|
||||
data = dict(Action='GetLastUpdatedTimeForRecommendations',
|
||||
MarketplaceId=marketplaceid)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
def list_recommendations(self, marketplaceid, recommendationcategory=None):
|
||||
"""
|
||||
Returns your active recommendations for a specific category or for all categories for a specific marketplace.
|
||||
"""
|
||||
|
||||
data = dict(Action="ListRecommendations",
|
||||
MarketplaceId=marketplaceid,
|
||||
RecommendationCategory=recommendationcategory)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
def list_recommendations_by_next_token(self, token):
|
||||
"""
|
||||
Returns the next page of recommendations using the NextToken parameter.
|
||||
"""
|
||||
|
||||
data = dict(Action="ListRecommendationsByNextToken",
|
||||
NextToken=token)
|
||||
return self.make_request(data, "POST")
|
||||
|
||||
class Finances(MWS):
|
||||
""" Amazon Finances API"""
|
||||
URI = '/Finances/2015-05-01'
|
||||
VERSION = '2015-05-01'
|
||||
NS = "{https://mws.amazonservices.com/Finances/2015-05-01}"
|
||||
|
||||
def list_financial_events(self , posted_after=None, posted_before=None,
|
||||
amazon_order_id=None, max_results='100'):
|
||||
|
||||
data = dict(Action='ListFinancialEvents',
|
||||
PostedAfter=posted_after,
|
||||
PostedBefore=posted_before,
|
||||
AmazonOrderId=amazon_order_id,
|
||||
MaxResultsPerPage=max_results,
|
||||
)
|
||||
return self.make_request(data)
|
@ -1,2 +0,0 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
@ -1,237 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2018-07-31 05:51:41.357047",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_amazon",
|
||||
"mws_credentials",
|
||||
"seller_id",
|
||||
"aws_access_key_id",
|
||||
"mws_auth_token",
|
||||
"secret_key",
|
||||
"column_break_4",
|
||||
"market_place_id",
|
||||
"region",
|
||||
"domain",
|
||||
"section_break_13",
|
||||
"company",
|
||||
"warehouse",
|
||||
"item_group",
|
||||
"price_list",
|
||||
"column_break_17",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"customer_type",
|
||||
"market_place_account_group",
|
||||
"section_break_12",
|
||||
"after_date",
|
||||
"taxes_charges",
|
||||
"sync_products",
|
||||
"sync_orders",
|
||||
"column_break_10",
|
||||
"enable_sync",
|
||||
"max_retry_limit"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_amazon",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Amazon"
|
||||
},
|
||||
{
|
||||
"fieldname": "mws_credentials",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "MWS Credentials"
|
||||
},
|
||||
{
|
||||
"fieldname": "seller_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Seller ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "aws_access_key_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "AWS Access Key ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mws_auth_token",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "MWS Auth Token",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secret_key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Secret Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "market_place_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Market Place ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "region",
|
||||
"fieldtype": "Select",
|
||||
"label": "Region",
|
||||
"options": "\nAE\nAU\nBR\nCA\nCN\nDE\nES\nFR\nIN\nJP\nIT\nMX\nUK\nUS",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "domain",
|
||||
"fieldtype": "Data",
|
||||
"label": "Domain",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Group",
|
||||
"options": "Customer Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "Territory",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Customer Type",
|
||||
"options": "Individual\nCompany",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "market_place_account_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Market Place Account Group",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Amazon will synch data updated after this date",
|
||||
"fieldname": "after_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "After Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Get financial breakup of Taxes and charges data by Amazon ",
|
||||
"fieldname": "taxes_charges",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sync Taxes and Charges"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "3",
|
||||
"fieldname": "max_retry_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max Retry Limit"
|
||||
},
|
||||
{
|
||||
"description": "Always sync your products from Amazon MWS before synching the Orders details",
|
||||
"fieldname": "sync_products",
|
||||
"fieldtype": "Button",
|
||||
"label": "Sync Products",
|
||||
"options": "get_products_details"
|
||||
},
|
||||
{
|
||||
"description": "Click this button to pull your Sales Order data from Amazon MWS.",
|
||||
"fieldname": "sync_orders",
|
||||
"fieldtype": "Button",
|
||||
"label": "Sync Orders",
|
||||
"options": "get_order_details"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Check this to enable a scheduled Daily synchronization routine via scheduler",
|
||||
"fieldname": "enable_sync",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Scheduled Sync"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-07 14:26:20.174848",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Amazon MWS Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import dateutil
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_orders
|
||||
|
||||
|
||||
class AmazonMWSSettings(Document):
|
||||
def validate(self):
|
||||
if self.enable_amazon == 1:
|
||||
self.enable_sync = 1
|
||||
setup_custom_fields()
|
||||
else:
|
||||
self.enable_sync = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_products_details(self):
|
||||
if self.enable_amazon == 1:
|
||||
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_order_details(self):
|
||||
if self.enable_amazon == 1:
|
||||
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
|
||||
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date)
|
||||
|
||||
def schedule_get_order_details():
|
||||
mws_settings = frappe.get_doc("Amazon MWS Settings")
|
||||
if mws_settings.enable_sync and mws_settings.enable_amazon:
|
||||
after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d")
|
||||
get_orders(after_date = after_date)
|
||||
|
||||
def setup_custom_fields():
|
||||
custom_fields = {
|
||||
"Item": [dict(fieldname='amazon_item_code', label='Amazon Item Code',
|
||||
fieldtype='Data', insert_after='series', read_only=1, print_hide=1)],
|
||||
"Sales Order": [dict(fieldname='amazon_order_id', label='Amazon Order ID',
|
||||
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields)
|
@ -1,8 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestAmazonMWSSettings(unittest.TestCase):
|
||||
pass
|
@ -1,104 +0,0 @@
|
||||
"""
|
||||
Created on Tue Jun 26 15:42:07 2012
|
||||
|
||||
Borrowed from https://github.com/timotheus/ebaysdk-python
|
||||
|
||||
@author: pierre
|
||||
"""
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class object_dict(dict):
|
||||
"""object view of dict, you can
|
||||
>>> a = object_dict()
|
||||
>>> a.fish = 'fish'
|
||||
>>> a['fish']
|
||||
'fish'
|
||||
>>> a['water'] = 'water'
|
||||
>>> a.water
|
||||
'water'
|
||||
>>> a.test = {'value': 1}
|
||||
>>> a.test2 = object_dict({'name': 'test2', 'value': 2})
|
||||
>>> a.test, a.test2.name, a.test2.value
|
||||
(1, 'test2', 2)
|
||||
"""
|
||||
def __init__(self, initd=None):
|
||||
if initd is None:
|
||||
initd = {}
|
||||
dict.__init__(self, initd)
|
||||
|
||||
def __getattr__(self, item):
|
||||
|
||||
try:
|
||||
d = self.__getitem__(item)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
if isinstance(d, dict) and 'value' in d and len(d) == 1:
|
||||
return d['value']
|
||||
else:
|
||||
return d
|
||||
|
||||
# if value is the only key in object, you can omit it
|
||||
def __setstate__(self, item):
|
||||
return False
|
||||
|
||||
def __setattr__(self, item, value):
|
||||
self.__setitem__(item, value)
|
||||
|
||||
def getvalue(self, item, value=None):
|
||||
return self.get(item, {}).get('value', value)
|
||||
|
||||
|
||||
class xml2dict(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _parse_node(self, node):
|
||||
node_tree = object_dict()
|
||||
# Save attrs and text, hope there will not be a child with same name
|
||||
if node.text:
|
||||
node_tree.value = node.text
|
||||
for (k, v) in node.attrib.items():
|
||||
k, v = self._namespace_split(k, object_dict({'value':v}))
|
||||
node_tree[k] = v
|
||||
#Save childrens
|
||||
for child in node.getchildren():
|
||||
tag, tree = self._namespace_split(child.tag,
|
||||
self._parse_node(child))
|
||||
if tag not in node_tree: # the first time, so store it in dict
|
||||
node_tree[tag] = tree
|
||||
continue
|
||||
old = node_tree[tag]
|
||||
if not isinstance(old, list):
|
||||
node_tree.pop(tag)
|
||||
node_tree[tag] = [old] # multi times, so change old dict to a list
|
||||
node_tree[tag].append(tree) # add the new one
|
||||
|
||||
return node_tree
|
||||
|
||||
def _namespace_split(self, tag, value):
|
||||
"""
|
||||
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
|
||||
ns = http://cs.sfsu.edu/csc867/myscheduler
|
||||
name = patients
|
||||
"""
|
||||
result = re.compile(r"\{(.*)\}(.*)").search(tag)
|
||||
if result:
|
||||
value.namespace, tag = result.groups()
|
||||
|
||||
return (tag, value)
|
||||
|
||||
def parse(self, file):
|
||||
"""parse a xml file to a dict"""
|
||||
f = open(file, 'r')
|
||||
return self.fromstring(f.read())
|
||||
|
||||
def fromstring(self, s):
|
||||
"""parse a string"""
|
||||
t = ET.fromstring(s)
|
||||
root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
|
||||
return object_dict({root_tag: root_tree})
|
@ -13,7 +13,7 @@ from frappe.utils import call_hook_method, cint, flt, get_url
|
||||
|
||||
|
||||
class GoCardlessSettings(Document):
|
||||
supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
|
||||
supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
|
||||
|
||||
def validate(self):
|
||||
self.initialize_client()
|
||||
@ -80,7 +80,7 @@ class GoCardlessSettings(Document):
|
||||
|
||||
def validate_transaction_currency(self, currency):
|
||||
if currency not in self.supported_currencies:
|
||||
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
|
||||
frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
|
||||
|
||||
def get_payment_url(self, **kwargs):
|
||||
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
|
||||
|
@ -29,17 +29,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Amazon MWS Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "Amazon MWS Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
|
@ -333,7 +333,6 @@ scheduler_events = {
|
||||
"hourly": [
|
||||
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
|
||||
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
@ -341,7 +340,8 @@ scheduler_events = {
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction"
|
||||
],
|
||||
"daily": [
|
||||
"erpnext.stock.reorder_item.reorder_item",
|
||||
|
@ -27,12 +27,13 @@
|
||||
"fetch_from": "employee.user_id",
|
||||
"fieldname": "user_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "ERPNext User ID",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-06-06 10:41:20.313756",
|
||||
"modified": "2022-02-13 19:44:21.302938",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Group Table",
|
||||
@ -42,4 +43,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
@ -546,7 +546,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
i = 0
|
||||
while(i<14):
|
||||
allocate_earned_leaves()
|
||||
allocate_earned_leaves(ignore_duplicates=True)
|
||||
i += 1
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
|
||||
@ -554,7 +554,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
|
||||
i = 0
|
||||
while(i<6):
|
||||
allocate_earned_leaves()
|
||||
allocate_earned_leaves(ignore_duplicates=True)
|
||||
i += 1
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
|
||||
|
@ -8,11 +8,10 @@ from math import ceil
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate
|
||||
from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate
|
||||
|
||||
|
||||
class LeavePolicyAssignment(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_policy_assignment_overlap()
|
||||
self.set_dates()
|
||||
@ -94,10 +93,12 @@ class LeavePolicyAssignment(Document):
|
||||
new_leaves_allocated = 0
|
||||
|
||||
elif leave_type_details.get(leave_type).is_earned_leave == 1:
|
||||
if self.assignment_based_on == "Leave Period":
|
||||
new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
|
||||
else:
|
||||
if not self.assignment_based_on:
|
||||
new_leaves_allocated = 0
|
||||
else:
|
||||
# get leaves for past months if assignment is based on Leave Period / Joining Date
|
||||
new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
|
||||
|
||||
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
|
||||
elif getdate(date_of_joining) > getdate(self.effective_from):
|
||||
remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
|
||||
@ -108,21 +109,24 @@ class LeavePolicyAssignment(Document):
|
||||
def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
|
||||
from erpnext.hr.utils import get_monthly_earned_leave
|
||||
|
||||
current_month = get_datetime().month
|
||||
current_year = get_datetime().year
|
||||
current_date = frappe.flags.current_date or getdate()
|
||||
if current_date > getdate(self.effective_to):
|
||||
current_date = getdate(self.effective_to)
|
||||
|
||||
from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
|
||||
if getdate(date_of_joining) > getdate(from_date):
|
||||
from_date = date_of_joining
|
||||
|
||||
from_date_month = get_datetime(from_date).month
|
||||
from_date_year = get_datetime(from_date).year
|
||||
from_date = getdate(self.effective_from)
|
||||
if getdate(date_of_joining) > from_date:
|
||||
from_date = getdate(date_of_joining)
|
||||
|
||||
months_passed = 0
|
||||
if current_year == from_date_year and current_month > from_date_month:
|
||||
months_passed = current_month - from_date_month
|
||||
elif current_year > from_date_year:
|
||||
months_passed = (12 - from_date_month) + current_month
|
||||
based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining
|
||||
|
||||
if current_date.year == from_date.year and current_date.month >= from_date.month:
|
||||
months_passed = current_date.month - from_date.month
|
||||
months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
|
||||
|
||||
elif current_date.year > from_date.year:
|
||||
months_passed = (12 - from_date.month) + current_date.month
|
||||
months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
|
||||
|
||||
if months_passed > 0:
|
||||
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
|
||||
@ -134,6 +138,23 @@ class LeavePolicyAssignment(Document):
|
||||
return new_leaves_allocated
|
||||
|
||||
|
||||
def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj):
|
||||
date = getdate(frappe.flags.current_date) or getdate()
|
||||
|
||||
if based_on_doj:
|
||||
# if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ,
|
||||
# then the month should be considered
|
||||
if date.day == date_of_joining.day:
|
||||
months_passed += 1
|
||||
else:
|
||||
last_day_of_month = get_last_day(date)
|
||||
# if its the last day of the month, then that month should be considered
|
||||
if last_day_of_month == date:
|
||||
months_passed += 1
|
||||
|
||||
return months_passed
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_assignment_for_multiple_employees(employees, data):
|
||||
|
||||
@ -168,7 +189,7 @@ def create_assignment_for_multiple_employees(employees, data):
|
||||
def get_leave_type_details():
|
||||
leave_type_details = frappe._dict()
|
||||
leave_types = frappe.get_all("Leave Type",
|
||||
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory",
|
||||
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining",
|
||||
"is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
|
||||
for d in leave_types:
|
||||
leave_type_details.setdefault(d.name, d)
|
||||
|
@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, get_first_day, getdate
|
||||
from frappe.utils import add_months, get_first_day, get_last_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
@ -20,36 +20,31 @@ test_dependencies = ["Employee"]
|
||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
def setUp(self):
|
||||
for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
||||
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
||||
frappe.db.delete(doctype)
|
||||
|
||||
employee = get_employee()
|
||||
self.original_doj = employee.date_of_joining
|
||||
self.employee = employee
|
||||
|
||||
def test_grant_leaves(self):
|
||||
leave_period = get_leave_period()
|
||||
employee = get_employee()
|
||||
|
||||
# create the leave policy with leave type "_Test Leave Type", allocation = 10
|
||||
# allocation = 10
|
||||
leave_policy = create_leave_policy()
|
||||
leave_policy.submit()
|
||||
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
|
||||
leave_policy_assignment_doc.reload()
|
||||
|
||||
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
|
||||
|
||||
leave_allocation = frappe.get_list("Leave Allocation", filters={
|
||||
"employee": employee.name,
|
||||
"employee": self.employee.name,
|
||||
"leave_policy":leave_policy.name,
|
||||
"leave_policy_assignment": leave_policy_assignments[0],
|
||||
"docstatus": 1})[0]
|
||||
|
||||
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
|
||||
|
||||
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
|
||||
@ -61,63 +56,46 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
|
||||
def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self):
|
||||
leave_period = get_leave_period()
|
||||
employee = get_employee()
|
||||
|
||||
# create the leave policy with leave type "_Test Leave Type", allocation = 10
|
||||
leave_policy = create_leave_policy()
|
||||
leave_policy.submit()
|
||||
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
|
||||
leave_policy_assignment_doc.reload()
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
# every leave is allocated no more leave can be granted now
|
||||
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
|
||||
leave_allocation = frappe.get_list("Leave Allocation", filters={
|
||||
"employee": employee.name,
|
||||
"employee": self.employee.name,
|
||||
"leave_policy":leave_policy.name,
|
||||
"leave_policy_assignment": leave_policy_assignments[0],
|
||||
"docstatus": 1})[0]
|
||||
|
||||
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
|
||||
|
||||
# User all allowed to grant leave when there is no allocation against assignment
|
||||
leave_alloc_doc.cancel()
|
||||
leave_alloc_doc.delete()
|
||||
|
||||
leave_policy_assignment_doc.reload()
|
||||
|
||||
|
||||
# User are now allowed to grant leave
|
||||
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
|
||||
self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0)
|
||||
|
||||
def test_earned_leave_allocation(self):
|
||||
leave_period = create_leave_period("Test Earned Leave Period")
|
||||
employee = get_employee()
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
|
||||
}).insert()
|
||||
}).submit()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
@ -125,11 +103,200 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 0)
|
||||
|
||||
def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self):
|
||||
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1)))
|
||||
|
||||
# Case 1: assignment created one month after the leave period, should allocate 1 leave
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 1)
|
||||
|
||||
def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self):
|
||||
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
|
||||
# Case 2: assignment created on the last day of the leave period's latter month
|
||||
# should allocate 1 leave for current month even though the month has not ended
|
||||
# since the daily job might have already executed
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
|
||||
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
|
||||
# initial leave allocation = 5
|
||||
leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave",
|
||||
from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0)
|
||||
leave_allocation.submit()
|
||||
|
||||
# Case 3: assignment created on the last day of the leave period's latter month with carry forwarding
|
||||
frappe.flags.current_date = get_last_day(add_months(getdate(), -1))
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
"carry_forward": 1
|
||||
}
|
||||
# carry forwarded leaves = 5, 3 leaves allocated for passed months
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
details = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True)
|
||||
self.assertEqual(details.new_leaves_allocated, 2)
|
||||
self.assertEqual(details.unused_leaves, 5)
|
||||
self.assertEqual(details.total_leaves_allocated, 7)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import is_earned_leave_already_allocated
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
|
||||
allocation = frappe.get_doc("Leave Allocation", details.name)
|
||||
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
|
||||
self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation))
|
||||
|
||||
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
|
||||
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
|
||||
}).submit()
|
||||
|
||||
# joining date set to 2 months back
|
||||
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
|
||||
self.employee.save()
|
||||
|
||||
# assignment created on the last day of the current month
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
data = {
|
||||
"assignment_based_on": "Joining Date",
|
||||
"leave_policy": leave_policy.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated")
|
||||
effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
|
||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
frappe.flags.current_date = get_last_day(getdate())
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
|
||||
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
|
||||
leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True)
|
||||
|
||||
# joining date set to 2 months back
|
||||
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
|
||||
self.employee.save()
|
||||
|
||||
# assignment created on the same day of the current month, should allocate leaves including the current month
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# if the daily job is not completed yet, there is another check present
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
|
||||
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
|
||||
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
|
||||
}).submit()
|
||||
|
||||
# joining date set to 2 months back
|
||||
# leave should be allocated for current month too since this day is same as the joining day
|
||||
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
|
||||
self.employee.save()
|
||||
|
||||
# assignment created on the first day of the current month
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
data = {
|
||||
"assignment_based_on": "Joining Date",
|
||||
"leave_policy": leave_policy.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated")
|
||||
effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
|
||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
# to ensure leave is not already allocated to avoid duplication
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
allocate_earned_leaves()
|
||||
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
||||
frappe.flags.current_date = None
|
||||
|
||||
|
||||
def create_earned_leave_type(leave_type):
|
||||
def create_earned_leave_type(leave_type, based_on_doj=False):
|
||||
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||
|
||||
return frappe.get_doc(dict(
|
||||
@ -138,13 +305,15 @@ def create_earned_leave_type(leave_type):
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6
|
||||
is_carry_forward=1,
|
||||
based_on_date_of_joining=based_on_doj
|
||||
)).insert()
|
||||
|
||||
|
||||
def create_leave_period(name):
|
||||
def create_leave_period(name, start_date=None):
|
||||
frappe.delete_doc_if_exists("Leave Period", name, force=1)
|
||||
start_date = get_first_day(getdate())
|
||||
if not start_date:
|
||||
start_date = get_first_day(getdate())
|
||||
|
||||
return frappe.get_doc(dict(
|
||||
name=name,
|
||||
@ -153,4 +322,17 @@ def create_leave_period(name):
|
||||
to_date=add_months(start_date, 12),
|
||||
company="_Test Company",
|
||||
is_active=1
|
||||
)).insert()
|
||||
)).insert()
|
||||
|
||||
|
||||
def setup_leave_period_and_policy(start_date, based_on_doj=False):
|
||||
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj)
|
||||
leave_period = create_leave_period("Test Earned Leave Period",
|
||||
start_date=start_date)
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
|
||||
}).insert()
|
||||
|
||||
return leave_period, leave_policy
|
@ -237,7 +237,7 @@ def generate_leave_encashment():
|
||||
|
||||
create_leave_encashment(leave_allocation=leave_allocation)
|
||||
|
||||
def allocate_earned_leaves():
|
||||
def allocate_earned_leaves(ignore_duplicates=False):
|
||||
'''Allocate earned leaves to Employees'''
|
||||
e_leave_types = get_earned_leaves()
|
||||
today = getdate()
|
||||
@ -261,13 +261,13 @@ def allocate_earned_leaves():
|
||||
|
||||
from_date=allocation.from_date
|
||||
|
||||
if e_leave_type.based_on_date_of_joining_date:
|
||||
if e_leave_type.based_on_date_of_joining:
|
||||
from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
|
||||
|
||||
if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
|
||||
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
|
||||
if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining):
|
||||
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates)
|
||||
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False):
|
||||
earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
|
||||
|
||||
allocation = frappe.get_doc('Leave Allocation', allocation.name)
|
||||
@ -277,9 +277,12 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type
|
||||
new_allocation = e_leave_type.max_leaves_allowed
|
||||
|
||||
if new_allocation != allocation.total_leaves_allocated:
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
today_date = today()
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
|
||||
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||
earned_leaves = 0.0
|
||||
@ -297,6 +300,28 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||
return earned_leaves
|
||||
|
||||
|
||||
def is_earned_leave_already_allocated(allocation, annual_allocation):
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
get_leave_type_details,
|
||||
)
|
||||
|
||||
leave_type_details = get_leave_type_details()
|
||||
date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
|
||||
|
||||
assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
|
||||
leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type,
|
||||
annual_allocation, leave_type_details, date_of_joining)
|
||||
|
||||
# exclude carry-forwarded leaves while checking for leave allocation for passed months
|
||||
num_allocations = allocation.total_leaves_allocated
|
||||
if allocation.unused_leaves:
|
||||
num_allocations -= allocation.unused_leaves
|
||||
|
||||
if num_allocations >= leaves_for_passed_months:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_leave_allocations(date, leave_type):
|
||||
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
|
||||
from `tabLeave Allocation`
|
||||
@ -318,7 +343,7 @@ def create_additional_leave_ledger_entry(allocation, leaves, date):
|
||||
allocation.unused_leaves = 0
|
||||
allocation.create_leave_ledger_entry()
|
||||
|
||||
def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date):
|
||||
def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining):
|
||||
import calendar
|
||||
|
||||
from dateutil import relativedelta
|
||||
@ -329,7 +354,7 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
|
||||
#last day of month
|
||||
last_day = calendar.monthrange(to_date.year, to_date.month)[1]
|
||||
|
||||
if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day):
|
||||
if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day):
|
||||
if frequency == "Monthly":
|
||||
return True
|
||||
elif frequency == "Quarterly" and rd.months % 3:
|
||||
|
@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', {
|
||||
});
|
||||
});
|
||||
|
||||
$.each(["payment_account", "loan_account"], function (i, field) {
|
||||
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
|
||||
frm.set_query(field, function () {
|
||||
return {
|
||||
"filters": {
|
||||
@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', {
|
||||
frm.add_custom_button(__('Loan Write Off'), function() {
|
||||
frm.trigger("make_loan_write_off_entry");
|
||||
},__('Create'));
|
||||
|
||||
frm.add_custom_button(__('Loan Refund'), function() {
|
||||
frm.trigger("make_loan_refund");
|
||||
},__('Create'));
|
||||
}
|
||||
}
|
||||
frm.trigger("toggle_fields");
|
||||
@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', {
|
||||
})
|
||||
},
|
||||
|
||||
make_loan_refund: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"loan": frm.doc.name
|
||||
},
|
||||
method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let doc = frappe.model.sync(r.message)[0];
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
request_loan_closure: function(frm) {
|
||||
frappe.confirm(__("Do you really want to close this loan"),
|
||||
function() {
|
||||
|
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "ACC-LOAN-.YYYY.-.#####",
|
||||
"creation": "2019-08-29 17:29:18.176786",
|
||||
"creation": "2022-01-25 10:30:02.294967",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
@ -34,6 +34,7 @@
|
||||
"is_term_loan",
|
||||
"account_info",
|
||||
"mode_of_payment",
|
||||
"disbursement_account",
|
||||
"payment_account",
|
||||
"column_break_9",
|
||||
"loan_account",
|
||||
@ -356,12 +357,21 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Closure Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_type.disbursement_account",
|
||||
"fieldname": "disbursement_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-12 18:10:32.360818",
|
||||
"modified": "2022-01-25 16:29:16.325501",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
@ -391,5 +401,6 @@
|
||||
"search_fields": "posting_date",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -10,6 +10,7 @@ from frappe import _
|
||||
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
|
||||
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
|
||||
@ -233,17 +234,15 @@ def request_loan_closure(loan, posting_date=None):
|
||||
loan_type = frappe.get_value('Loan', loan, 'loan_type')
|
||||
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
|
||||
|
||||
# checking greater than 0 as there may be some minor precision error
|
||||
if not pending_amount:
|
||||
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
|
||||
elif pending_amount < write_off_limit:
|
||||
if pending_amount and abs(pending_amount) < write_off_limit:
|
||||
# Auto create loan write off and update status as loan closure requested
|
||||
write_off = make_loan_write_off(loan)
|
||||
write_off.submit()
|
||||
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
|
||||
else:
|
||||
elif pending_amount > 0:
|
||||
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
|
||||
|
||||
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_loan_application(loan_application):
|
||||
loan = frappe.get_doc("Loan Application", loan_application)
|
||||
@ -400,4 +399,39 @@ def add_single_month(date):
|
||||
if getdate(date) == get_last_day(date):
|
||||
return get_last_day(add_months(date, 1))
|
||||
else:
|
||||
return add_months(date, 1)
|
||||
return add_months(date, 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
|
||||
loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant',
|
||||
'loan_account', 'payment_account', 'posting_date', 'company', 'name',
|
||||
'total_payment', 'total_principal_paid'], as_dict=1)
|
||||
|
||||
loan_details.doctype = 'Loan'
|
||||
loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
|
||||
|
||||
if not amount:
|
||||
amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
|
||||
|
||||
if amount < 0:
|
||||
frappe.throw(_('No excess amount pending for refund'))
|
||||
|
||||
refund_jv = get_payment_entry(loan_details, {
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party_account": loan_details.loan_account,
|
||||
"amount_field_party": 'debit_in_account_currency',
|
||||
"amount_field_bank": 'credit_in_account_currency',
|
||||
"amount": amount,
|
||||
"bank_account": loan_details.payment_account
|
||||
})
|
||||
|
||||
if reference_number:
|
||||
refund_jv.cheque_no = reference_number
|
||||
|
||||
if reference_date:
|
||||
refund_jv.cheque_date = reference_date
|
||||
|
||||
if submit:
|
||||
refund_jv.submit()
|
||||
|
||||
return refund_jv
|
@ -42,16 +42,17 @@ class TestLoan(unittest.TestCase):
|
||||
create_loan_type("Personal Loan", 500000, 8.4,
|
||||
is_term_loan=1,
|
||||
mode_of_payment='Cash',
|
||||
disbursement_account='Disbursement Account - _TC',
|
||||
payment_account='Payment Account - _TC',
|
||||
loan_account='Loan Account - _TC',
|
||||
interest_income_account='Interest Income Account - _TC',
|
||||
penalty_income_account='Penalty Income Account - _TC')
|
||||
|
||||
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_security_type()
|
||||
create_loan_security()
|
||||
@ -679,6 +680,29 @@ class TestLoan(unittest.TestCase):
|
||||
loan.load_from_db()
|
||||
self.assertEqual(loan.status, "Loan Closure Requested")
|
||||
|
||||
def test_loan_repayment_against_partially_disbursed_loan(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
"qty": 4000.00
|
||||
}]
|
||||
|
||||
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
|
||||
create_pledge(loan_application)
|
||||
|
||||
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
|
||||
loan.submit()
|
||||
|
||||
first_date = '2019-10-01'
|
||||
last_date = '2019-10-30'
|
||||
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date)
|
||||
|
||||
loan.load_from_db()
|
||||
|
||||
self.assertEqual(loan.status, "Partially Disbursed")
|
||||
create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
|
||||
flt(loan.loan_amount/3))
|
||||
|
||||
def test_loan_amount_write_off(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
@ -790,6 +814,18 @@ def create_loan_accounts():
|
||||
"account_type": "Bank",
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists("Account", "Disbursement Account - _TC"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"company": "_Test Company",
|
||||
"account_name": "Disbursement Account",
|
||||
"root_type": "Asset",
|
||||
"report_type": "Balance Sheet",
|
||||
"currency": "INR",
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
"account_type": "Bank",
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists("Account", "Interest Income Account - _TC"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
@ -815,7 +851,7 @@ def create_loan_accounts():
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
|
||||
mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
|
||||
mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
|
||||
repayment_method=None, repayment_periods=None):
|
||||
|
||||
if not frappe.db.exists("Loan Type", loan_name):
|
||||
@ -829,6 +865,7 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i
|
||||
"penalty_interest_rate": penalty_interest_rate,
|
||||
"grace_period_in_days": grace_period_in_days,
|
||||
"mode_of_payment": mode_of_payment,
|
||||
"disbursement_account": disbursement_account,
|
||||
"payment_account": payment_account,
|
||||
"loan_account": loan_account,
|
||||
"interest_income_account": interest_income_account,
|
||||
|
@ -15,7 +15,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
class TestLoanApplication(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_loan_accounts()
|
||||
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
|
||||
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
|
||||
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')
|
||||
|
@ -122,7 +122,7 @@ class LoanDisbursement(AccountsController):
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.loan_account,
|
||||
"against": loan_details.payment_account,
|
||||
"against": loan_details.disbursement_account,
|
||||
"debit": self.disbursed_amount,
|
||||
"debit_in_account_currency": self.disbursed_amount,
|
||||
"against_voucher_type": "Loan",
|
||||
@ -137,7 +137,7 @@ class LoanDisbursement(AccountsController):
|
||||
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.payment_account,
|
||||
"account": loan_details.disbursement_account,
|
||||
"against": loan_details.loan_account,
|
||||
"credit": self.disbursed_amount,
|
||||
"credit_in_account_currency": self.disbursed_amount,
|
||||
|
@ -44,8 +44,8 @@ class TestLoanDisbursement(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_loan_accounts()
|
||||
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_security_type()
|
||||
create_loan_security()
|
||||
|
@ -30,8 +30,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_loan_accounts()
|
||||
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
|
||||
'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
|
||||
'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
|
||||
|
||||
create_loan_security_type()
|
||||
create_loan_security()
|
||||
|
@ -125,7 +125,7 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
def update_paid_amount(self):
|
||||
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
|
||||
'written_off_amount'], as_dict=1)
|
||||
|
||||
loan.update({
|
||||
@ -153,7 +153,7 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
def mark_as_unpaid(self):
|
||||
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
|
||||
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
|
||||
'written_off_amount'], as_dict=1)
|
||||
|
||||
no_of_repayments = len(self.repayment_details)
|
||||
@ -345,7 +345,7 @@ class LoanRepayment(AccountsController):
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
"account": loan_details.penalty_income_account,
|
||||
"against": payment_account,
|
||||
"against": loan_details.loan_account,
|
||||
"credit": self.total_penalty_paid,
|
||||
"credit_in_account_currency": self.total_penalty_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
@ -367,7 +367,9 @@ class LoanRepayment(AccountsController):
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": remarks,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date)
|
||||
"posting_date": getdate(self.posting_date),
|
||||
"party_type": loan_details.applicant_type if self.repay_from_salary else '',
|
||||
"party": loan_details.applicant if self.repay_from_salary else ''
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -15,7 +15,7 @@ frappe.ui.form.on('Loan Type', {
|
||||
});
|
||||
});
|
||||
|
||||
$.each(["payment_account", "loan_account"], function (i, field) {
|
||||
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
|
||||
frm.set_query(field, function () {
|
||||
return {
|
||||
"filters": {
|
||||
|
@ -19,9 +19,10 @@
|
||||
"description",
|
||||
"account_details_section",
|
||||
"mode_of_payment",
|
||||
"disbursement_account",
|
||||
"payment_account",
|
||||
"loan_account",
|
||||
"column_break_12",
|
||||
"loan_account",
|
||||
"interest_income_account",
|
||||
"penalty_income_account",
|
||||
"amended_from"
|
||||
@ -79,7 +80,7 @@
|
||||
{
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Account",
|
||||
"label": "Repayment Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
@ -149,15 +150,23 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Auto Write Off Amount ",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "disbursement_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:10:57.368490",
|
||||
"modified": "2022-01-25 16:23:57.009349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Type",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -181,5 +190,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -28,9 +28,24 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
|
||||
class ProductionPlan(Document):
|
||||
def validate(self):
|
||||
self.set_pending_qty_in_row_without_reference()
|
||||
self.calculate_total_planned_qty()
|
||||
self.set_status()
|
||||
|
||||
def set_pending_qty_in_row_without_reference(self):
|
||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||
if self.docstatus > 0: # set only to initialise value before submit
|
||||
return
|
||||
|
||||
for item in self.po_items:
|
||||
if not item.get("sales_order") or not item.get("material_request"):
|
||||
item.pending_qty = item.planned_qty
|
||||
|
||||
def calculate_total_planned_qty(self):
|
||||
self.total_planned_qty = 0
|
||||
for d in self.po_items:
|
||||
self.total_planned_qty += flt(d.planned_qty)
|
||||
|
||||
def validate_data(self):
|
||||
for d in self.get('po_items'):
|
||||
if not d.bom_no:
|
||||
@ -263,11 +278,6 @@ class ProductionPlan(Document):
|
||||
'qty': so_detail['qty']
|
||||
})
|
||||
|
||||
def calculate_total_planned_qty(self):
|
||||
self.total_planned_qty = 0
|
||||
for d in self.po_items:
|
||||
self.total_planned_qty += flt(d.planned_qty)
|
||||
|
||||
def calculate_total_produced_qty(self):
|
||||
self.total_produced_qty = 0
|
||||
for d in self.po_items:
|
||||
@ -275,10 +285,11 @@ class ProductionPlan(Document):
|
||||
|
||||
self.db_set("total_produced_qty", self.total_produced_qty, update_modified=False)
|
||||
|
||||
def update_produced_qty(self, produced_qty, production_plan_item):
|
||||
def update_produced_pending_qty(self, produced_qty, production_plan_item):
|
||||
for data in self.po_items:
|
||||
if data.name == production_plan_item:
|
||||
data.produced_qty = produced_qty
|
||||
data.pending_qty = flt(data.planned_qty - produced_qty)
|
||||
data.db_update()
|
||||
|
||||
self.calculate_total_produced_qty()
|
||||
@ -341,6 +352,7 @@ class ProductionPlan(Document):
|
||||
|
||||
def get_production_items(self):
|
||||
item_dict = {}
|
||||
|
||||
for d in self.po_items:
|
||||
item_details = {
|
||||
"production_item" : d.item_code,
|
||||
@ -357,12 +369,12 @@ class ProductionPlan(Document):
|
||||
"production_plan" : self.name,
|
||||
"production_plan_item" : d.name,
|
||||
"product_bundle_item" : d.product_bundle_item,
|
||||
"planned_start_date" : d.planned_start_date
|
||||
"planned_start_date" : d.planned_start_date,
|
||||
"project" : self.project
|
||||
}
|
||||
|
||||
item_details.update({
|
||||
"project": self.project or frappe.db.get_value("Sales Order", d.sales_order, "project")
|
||||
})
|
||||
if not item_details['project'] and d.sales_order:
|
||||
item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
|
||||
|
||||
if self.get_items_from == "Material Request":
|
||||
item_details.update({
|
||||
@ -380,39 +392,59 @@ class ProductionPlan(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_work_order(self):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse
|
||||
|
||||
wo_list, po_list = [], []
|
||||
subcontracted_po = {}
|
||||
default_warehouses = get_default_warehouse()
|
||||
|
||||
self.validate_data()
|
||||
self.make_work_order_for_finished_goods(wo_list)
|
||||
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
|
||||
self.make_work_order_for_finished_goods(wo_list, default_warehouses)
|
||||
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses)
|
||||
self.make_subcontracted_purchase_order(subcontracted_po, po_list)
|
||||
self.show_list_created_message('Work Order', wo_list)
|
||||
self.show_list_created_message('Purchase Order', po_list)
|
||||
|
||||
def make_work_order_for_finished_goods(self, wo_list):
|
||||
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
|
||||
items_data = self.get_production_items()
|
||||
|
||||
for key, item in items_data.items():
|
||||
if self.sub_assembly_items:
|
||||
item['use_multi_level_bom'] = 0
|
||||
|
||||
set_default_warehouses(item, default_warehouses)
|
||||
work_order = self.create_work_order(item)
|
||||
if work_order:
|
||||
wo_list.append(work_order)
|
||||
|
||||
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
|
||||
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses):
|
||||
for row in self.sub_assembly_items:
|
||||
if row.type_of_manufacturing == 'Subcontract':
|
||||
subcontracted_po.setdefault(row.supplier, []).append(row)
|
||||
continue
|
||||
|
||||
args = {}
|
||||
self.prepare_args_for_sub_assembly_items(row, args)
|
||||
work_order = self.create_work_order(args)
|
||||
work_order_data = {
|
||||
'wip_warehouse': default_warehouses.get('wip_warehouse'),
|
||||
'fg_warehouse': default_warehouses.get('fg_warehouse')
|
||||
}
|
||||
|
||||
self.prepare_data_for_sub_assembly_items(row, work_order_data)
|
||||
work_order = self.create_work_order(work_order_data)
|
||||
if work_order:
|
||||
wo_list.append(work_order)
|
||||
|
||||
def prepare_data_for_sub_assembly_items(self, row, wo_data):
|
||||
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||
"description", "bom_no", "stock_uom", "bom_level",
|
||||
"production_plan_item", "schedule_date"]:
|
||||
if row.get(field):
|
||||
wo_data[field] = row.get(field)
|
||||
|
||||
wo_data.update({
|
||||
"use_multi_level_bom": 0,
|
||||
"production_plan": self.name,
|
||||
"production_plan_sub_assembly_item": row.name
|
||||
})
|
||||
|
||||
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
|
||||
if not subcontracted_po:
|
||||
return
|
||||
@ -423,7 +455,7 @@ class ProductionPlan(Document):
|
||||
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
|
||||
po.is_subcontracted = 'Yes'
|
||||
for row in po_list:
|
||||
args = {
|
||||
po_data = {
|
||||
'item_code': row.production_item,
|
||||
'warehouse': row.fg_warehouse,
|
||||
'production_plan_sub_assembly_item': row.name,
|
||||
@ -433,9 +465,9 @@ class ProductionPlan(Document):
|
||||
|
||||
for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
|
||||
'description', 'production_plan_item']:
|
||||
args[field] = row.get(field)
|
||||
po_data[field] = row.get(field)
|
||||
|
||||
po.append('items', args)
|
||||
po.append('items', po_data)
|
||||
|
||||
po.set_missing_values()
|
||||
po.flags.ignore_mandatory = True
|
||||
@ -452,24 +484,9 @@ class ProductionPlan(Document):
|
||||
doc_list = [get_link_to_form(doctype, p) for p in doc_list]
|
||||
msgprint(_("{0} created").format(comma_and(doc_list)))
|
||||
|
||||
def prepare_args_for_sub_assembly_items(self, row, args):
|
||||
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||
"description", "bom_no", "stock_uom", "bom_level",
|
||||
"production_plan_item", "schedule_date"]:
|
||||
args[field] = row.get(field)
|
||||
|
||||
args.update({
|
||||
"use_multi_level_bom": 0,
|
||||
"production_plan": self.name,
|
||||
"production_plan_sub_assembly_item": row.name
|
||||
})
|
||||
|
||||
def create_work_order(self, item):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
OverProductionError,
|
||||
get_default_warehouse,
|
||||
)
|
||||
warehouse = get_default_warehouse()
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
|
||||
|
||||
wo = frappe.new_doc("Work Order")
|
||||
wo.update(item)
|
||||
wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date')
|
||||
@ -478,11 +495,11 @@ class ProductionPlan(Document):
|
||||
wo.fg_warehouse = item.get("warehouse")
|
||||
|
||||
wo.set_work_order_operations()
|
||||
wo.set_required_items()
|
||||
|
||||
if not wo.fg_warehouse:
|
||||
wo.fg_warehouse = warehouse.get('fg_warehouse')
|
||||
try:
|
||||
wo.flags.ignore_mandatory = True
|
||||
wo.flags.ignore_validate = True
|
||||
wo.insert()
|
||||
return wo.name
|
||||
except OverProductionError:
|
||||
@ -1023,3 +1040,8 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
for field in ['wip_warehouse', 'fg_warehouse']:
|
||||
if not row.get(field):
|
||||
row[field] = default_warehouses.get(field)
|
@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
@ -36,15 +37,21 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
if not frappe.db.get_value('BOM', {'item': item}):
|
||||
make_bom(item = item, raw_materials = raw_materials)
|
||||
|
||||
def test_production_plan(self):
|
||||
def test_production_plan_mr_creation(self):
|
||||
"Test if MRs are created for unavailable raw materials."
|
||||
pln = create_production_plan(item_code='Test Production Item 1')
|
||||
self.assertTrue(len(pln.mr_items), 2)
|
||||
pln.make_material_request()
|
||||
|
||||
pln = frappe.get_doc('Production Plan', pln.name)
|
||||
pln.make_material_request()
|
||||
pln.reload()
|
||||
self.assertTrue(pln.status, 'Material Requested')
|
||||
material_requests = frappe.get_all('Material Request Item', fields = ['distinct parent'],
|
||||
filters = {'production_plan': pln.name}, as_list=1)
|
||||
|
||||
material_requests = frappe.get_all(
|
||||
'Material Request Item',
|
||||
fields = ['distinct parent'],
|
||||
filters = {'production_plan': pln.name},
|
||||
as_list=1
|
||||
)
|
||||
|
||||
self.assertTrue(len(material_requests), 2)
|
||||
|
||||
@ -66,27 +73,42 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
pln.cancel()
|
||||
|
||||
def test_production_plan_start_date(self):
|
||||
"Test if Work Order has same Planned Start Date as Prod Plan."
|
||||
planned_date = add_to_date(date=None, days=3)
|
||||
plan = create_production_plan(item_code='Test Production Item 1', planned_start_date=planned_date)
|
||||
plan = create_production_plan(
|
||||
item_code='Test Production Item 1',
|
||||
planned_start_date=planned_date
|
||||
)
|
||||
plan.make_work_order()
|
||||
|
||||
work_orders = frappe.get_all('Work Order', fields = ['name', 'planned_start_date'],
|
||||
filters = {'production_plan': plan.name})
|
||||
work_orders = frappe.get_all(
|
||||
'Work Order',
|
||||
fields = ['name', 'planned_start_date'],
|
||||
filters = {'production_plan': plan.name}
|
||||
)
|
||||
|
||||
self.assertEqual(work_orders[0].planned_start_date, planned_date)
|
||||
|
||||
for wo in work_orders:
|
||||
frappe.delete_doc('Work Order', wo.name)
|
||||
|
||||
frappe.get_doc('Production Plan', plan.name).cancel()
|
||||
plan.reload()
|
||||
plan.cancel()
|
||||
|
||||
def test_production_plan_for_existing_ordered_qty(self):
|
||||
"""
|
||||
- Enable 'ignore_existing_ordered_qty'.
|
||||
- Test if MR Planning table pulls Raw Material Qty even if it is in stock.
|
||||
"""
|
||||
sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
|
||||
target="_Test Warehouse - _TC", qty=1, rate=110)
|
||||
sr2 = create_stock_reconciliation(item_code="Raw Material Item 2",
|
||||
target="_Test Warehouse - _TC", qty=1, rate=120)
|
||||
|
||||
pln = create_production_plan(item_code='Test Production Item 1', ignore_existing_ordered_qty=0)
|
||||
pln = create_production_plan(
|
||||
item_code='Test Production Item 1',
|
||||
ignore_existing_ordered_qty=1
|
||||
)
|
||||
self.assertTrue(len(pln.mr_items), 1)
|
||||
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
|
||||
|
||||
@ -95,23 +117,39 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
pln.cancel()
|
||||
|
||||
def test_production_plan_with_non_stock_item(self):
|
||||
pln = create_production_plan(item_code='Test Production Item 1', include_non_stock_items=0)
|
||||
"Test if MR Planning table includes Non Stock RM."
|
||||
pln = create_production_plan(
|
||||
item_code='Test Production Item 1',
|
||||
include_non_stock_items=1
|
||||
)
|
||||
self.assertTrue(len(pln.mr_items), 3)
|
||||
pln.cancel()
|
||||
|
||||
def test_production_plan_without_multi_level(self):
|
||||
pln = create_production_plan(item_code='Test Production Item 1', use_multi_level_bom=0)
|
||||
"Test MR Planning table for non exploded BOM."
|
||||
pln = create_production_plan(
|
||||
item_code='Test Production Item 1',
|
||||
use_multi_level_bom=0
|
||||
)
|
||||
self.assertTrue(len(pln.mr_items), 2)
|
||||
pln.cancel()
|
||||
|
||||
def test_production_plan_without_multi_level_for_existing_ordered_qty(self):
|
||||
"""
|
||||
- Disable 'ignore_existing_ordered_qty'.
|
||||
- Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for
|
||||
non exploded BOM.
|
||||
"""
|
||||
sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
|
||||
target="_Test Warehouse - _TC", qty=1, rate=130)
|
||||
sr2 = create_stock_reconciliation(item_code="Subassembly Item 1",
|
||||
target="_Test Warehouse - _TC", qty=1, rate=140)
|
||||
|
||||
pln = create_production_plan(item_code='Test Production Item 1',
|
||||
use_multi_level_bom=0, ignore_existing_ordered_qty=0)
|
||||
pln = create_production_plan(
|
||||
item_code='Test Production Item 1',
|
||||
use_multi_level_bom=0,
|
||||
ignore_existing_ordered_qty=0
|
||||
)
|
||||
self.assertTrue(len(pln.mr_items), 0)
|
||||
|
||||
sr1.cancel()
|
||||
@ -119,6 +157,7 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
pln.cancel()
|
||||
|
||||
def test_production_plan_sales_orders(self):
|
||||
"Test if previously fulfilled SO (with WO) is pulled into Prod Plan."
|
||||
item = 'Test Production Item 1'
|
||||
so = make_sales_order(item_code=item, qty=1)
|
||||
sales_order = so.name
|
||||
@ -166,24 +205,25 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
self.assertEqual(sales_orders, [])
|
||||
|
||||
def test_production_plan_combine_items(self):
|
||||
"Test combining FG items in Production Plan."
|
||||
item = 'Test Production Item 1'
|
||||
so = make_sales_order(item_code=item, qty=1)
|
||||
so1 = make_sales_order(item_code=item, qty=1)
|
||||
|
||||
pln = frappe.new_doc('Production Plan')
|
||||
pln.company = so.company
|
||||
pln.company = so1.company
|
||||
pln.get_items_from = 'Sales Order'
|
||||
pln.append('sales_orders', {
|
||||
'sales_order': so.name,
|
||||
'sales_order_date': so.transaction_date,
|
||||
'customer': so.customer,
|
||||
'grand_total': so.grand_total
|
||||
'sales_order': so1.name,
|
||||
'sales_order_date': so1.transaction_date,
|
||||
'customer': so1.customer,
|
||||
'grand_total': so1.grand_total
|
||||
})
|
||||
so = make_sales_order(item_code=item, qty=2)
|
||||
so2 = make_sales_order(item_code=item, qty=2)
|
||||
pln.append('sales_orders', {
|
||||
'sales_order': so.name,
|
||||
'sales_order_date': so.transaction_date,
|
||||
'customer': so.customer,
|
||||
'grand_total': so.grand_total
|
||||
'sales_order': so2.name,
|
||||
'sales_order_date': so2.transaction_date,
|
||||
'customer': so2.customer,
|
||||
'grand_total': so2.grand_total
|
||||
})
|
||||
pln.combine_items = 1
|
||||
pln.get_items()
|
||||
@ -214,28 +254,37 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
|
||||
self.assertEqual(so_wo_qty, 0.0)
|
||||
|
||||
latest_plan = frappe.get_doc('Production Plan', pln.name)
|
||||
latest_plan.cancel()
|
||||
pln.reload()
|
||||
pln.cancel()
|
||||
|
||||
def test_pp_to_mr_customer_provided(self):
|
||||
#Material Request from Production Plan for Customer Provided
|
||||
" Test Material Request from Production Plan for Customer Provided Item."
|
||||
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
|
||||
create_item('Production Item CUST')
|
||||
|
||||
for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items():
|
||||
if not frappe.db.get_value('BOM', {'item': item}):
|
||||
make_bom(item = item, raw_materials = raw_materials)
|
||||
production_plan = create_production_plan(item_code = 'Production Item CUST')
|
||||
production_plan.make_material_request()
|
||||
material_request = frappe.db.get_value('Material Request Item', {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, 'parent')
|
||||
|
||||
material_request = frappe.db.get_value(
|
||||
'Material Request Item',
|
||||
{'production_plan': production_plan.name, 'item_code': 'CUST-0987'},
|
||||
'parent'
|
||||
)
|
||||
mr = frappe.get_doc('Material Request', material_request)
|
||||
|
||||
self.assertTrue(mr.material_request_type, 'Customer Provided')
|
||||
self.assertTrue(mr.customer, '_Test Customer')
|
||||
|
||||
def test_production_plan_with_multi_level_bom(self):
|
||||
#|Item Code | Qty |
|
||||
#|Test BOM 1 | 1 |
|
||||
#| Test BOM 2 | 2 |
|
||||
#| Test BOM 3 | 3 |
|
||||
"""
|
||||
Item Code | Qty |
|
||||
|Test BOM 1 | 1 |
|
||||
|Test BOM 2 | 2 |
|
||||
|Test BOM 3 | 3 |
|
||||
"""
|
||||
|
||||
for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]:
|
||||
create_item(item_code, is_stock_item=1)
|
||||
@ -264,15 +313,18 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
pln.make_work_order()
|
||||
|
||||
#last level sub-assembly work order produce qty
|
||||
to_produce_qty = frappe.db.get_value("Work Order",
|
||||
{"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty")
|
||||
to_produce_qty = frappe.db.get_value(
|
||||
"Work Order",
|
||||
{"production_plan": pln.name, "production_item": "Test BOM 3"},
|
||||
"qty"
|
||||
)
|
||||
|
||||
self.assertEqual(to_produce_qty, 18.0)
|
||||
pln.cancel()
|
||||
frappe.delete_doc("Production Plan", pln.name)
|
||||
|
||||
def test_get_warehouse_list_group(self):
|
||||
"""Check if required warehouses are returned"""
|
||||
"Check if required child warehouses are returned."
|
||||
warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
|
||||
|
||||
warehouses = set(get_warehouse_list(warehouse_json))
|
||||
@ -284,6 +336,7 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
|
||||
|
||||
def test_get_warehouse_list_single(self):
|
||||
"Check if same warehouse is returned in absence of child warehouses."
|
||||
warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
|
||||
|
||||
warehouses = set(get_warehouse_list(warehouse_json))
|
||||
@ -292,6 +345,7 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
self.assertEqual(warehouses, expected_warehouses)
|
||||
|
||||
def test_get_sales_order_with_variant(self):
|
||||
"Check if Template BOM is fetched in absence of Variant BOM."
|
||||
rm_item = create_item('PIV_RM', valuation_rate = 100)
|
||||
if not frappe.db.exists('Item', {"item_code": 'PIV'}):
|
||||
item = create_item('PIV', valuation_rate = 100)
|
||||
@ -348,7 +402,7 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_subassmebly_sorting(self):
|
||||
""" Test subassembly sorting in case of multiple items with nested BOMs"""
|
||||
"Test subassembly sorting in case of multiple items with nested BOMs."
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
prefix = "_TestLevel_"
|
||||
@ -386,6 +440,7 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
|
||||
|
||||
def test_multiple_work_order_for_production_plan_item(self):
|
||||
"Test producing Prod Plan (making WO) in parts."
|
||||
def create_work_order(item, pln, qty):
|
||||
# Get Production Items
|
||||
items_data = pln.get_production_items()
|
||||
@ -441,7 +496,107 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].ordered_qty, 0)
|
||||
|
||||
def test_production_plan_pending_qty_with_sales_order(self):
|
||||
"""
|
||||
Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel)
|
||||
"""
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_se_from_wo,
|
||||
)
|
||||
|
||||
make_stock_entry(item_code="Raw Material Item 1",
|
||||
target="Work In Progress - _TC",
|
||||
qty=2, basic_rate=100
|
||||
)
|
||||
make_stock_entry(item_code="Raw Material Item 2",
|
||||
target="Work In Progress - _TC",
|
||||
qty=2, basic_rate=100
|
||||
)
|
||||
|
||||
item = 'Test Production Item 1'
|
||||
so = make_sales_order(item_code=item, qty=1)
|
||||
|
||||
pln = create_production_plan(
|
||||
company=so.company,
|
||||
get_items_from="Sales Order",
|
||||
sales_order=so,
|
||||
skip_getting_mr_items=True
|
||||
)
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 1)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item_code=item, qty=1,
|
||||
company=so.company,
|
||||
wip_warehouse='Work In Progress - _TC',
|
||||
fg_warehouse='Finished Goods - _TC',
|
||||
skip_transfer=1,
|
||||
do_not_submit=True
|
||||
)
|
||||
wo.production_plan = pln.name
|
||||
wo.production_plan_item = pln.po_items[0].name
|
||||
wo.submit()
|
||||
|
||||
se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
|
||||
se.submit()
|
||||
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 0)
|
||||
|
||||
se.cancel()
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 1)
|
||||
|
||||
def test_production_plan_pending_qty_independent_items(self):
|
||||
"Test Prod Plan impact if items are added independently (no from SO or MR)."
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_se_from_wo,
|
||||
)
|
||||
|
||||
make_stock_entry(item_code="Raw Material Item 1",
|
||||
target="Work In Progress - _TC",
|
||||
qty=2, basic_rate=100
|
||||
)
|
||||
make_stock_entry(item_code="Raw Material Item 2",
|
||||
target="Work In Progress - _TC",
|
||||
qty=2, basic_rate=100
|
||||
)
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code='Test Production Item 1',
|
||||
skip_getting_mr_items=True
|
||||
)
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 1)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item_code='Test Production Item 1', qty=1,
|
||||
company=pln.company,
|
||||
wip_warehouse='Work In Progress - _TC',
|
||||
fg_warehouse='Finished Goods - _TC',
|
||||
skip_transfer=1,
|
||||
do_not_submit=True
|
||||
)
|
||||
wo.production_plan = pln.name
|
||||
wo.production_plan_item = pln.po_items[0].name
|
||||
wo.submit()
|
||||
|
||||
se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1))
|
||||
se.submit()
|
||||
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 0)
|
||||
|
||||
se.cancel()
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 1)
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
sales_order (obj): Sales Order Doc Object
|
||||
get_items_from (str): Sales Order/Material Request
|
||||
skip_getting_mr_items (bool): Whether or not to plan for new MRs
|
||||
"""
|
||||
args = frappe._dict(args)
|
||||
|
||||
pln = frappe.get_doc({
|
||||
@ -449,20 +604,35 @@ def create_production_plan(**args):
|
||||
'company': args.company or '_Test Company',
|
||||
'customer': args.customer or '_Test Customer',
|
||||
'posting_date': nowdate(),
|
||||
'include_non_stock_items': args.include_non_stock_items or 1,
|
||||
'include_subcontracted_items': args.include_subcontracted_items or 1,
|
||||
'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 1,
|
||||
'po_items': [{
|
||||
'include_non_stock_items': args.include_non_stock_items or 0,
|
||||
'include_subcontracted_items': args.include_subcontracted_items or 0,
|
||||
'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0,
|
||||
'get_items_from': 'Sales Order'
|
||||
})
|
||||
|
||||
if not args.get("sales_order"):
|
||||
pln.append('po_items', {
|
||||
'use_multi_level_bom': args.use_multi_level_bom or 1,
|
||||
'item_code': args.item_code,
|
||||
'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'),
|
||||
'planned_qty': args.planned_qty or 1,
|
||||
'planned_start_date': args.planned_start_date or now_datetime()
|
||||
}]
|
||||
})
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
pln.append('mr_items', d)
|
||||
})
|
||||
|
||||
if args.get("get_items_from") == "Sales Order" and args.get("sales_order"):
|
||||
so = args.get("sales_order")
|
||||
pln.append('sales_orders', {
|
||||
'sales_order': so.name,
|
||||
'sales_order_date': so.transaction_date,
|
||||
'customer': so.customer,
|
||||
'grand_total': so.grand_total
|
||||
})
|
||||
pln.get_items()
|
||||
|
||||
if not args.get("skip_getting_mr_items"):
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
pln.append('mr_items', d)
|
||||
|
||||
if not args.do_not_save:
|
||||
pln.insert()
|
||||
|
@ -201,6 +201,21 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production),
|
||||
cint(bin1_on_start_production.reserved_qty_for_production))
|
||||
|
||||
def test_reserved_qty_for_production_closed(self):
|
||||
|
||||
wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2,
|
||||
source_warehouse=self.warehouse)
|
||||
item = wo1.required_items[0].item_code
|
||||
bin_before = get_bin(item, self.warehouse)
|
||||
bin_before.update_reserved_qty_for_production()
|
||||
|
||||
make_wo_order_test_record(item="_Test FG Item", qty=2,
|
||||
source_warehouse=self.warehouse)
|
||||
close_work_order(wo1.name, "Closed")
|
||||
|
||||
bin_after = get_bin(item, self.warehouse)
|
||||
self.assertEqual(bin_before.reserved_qty_for_production, bin_after.reserved_qty_for_production)
|
||||
|
||||
def test_backflush_qty_for_overpduction_manufacture(self):
|
||||
cancel_stock_entry = []
|
||||
allow_overproduction("overproduction_percentage_for_work_order", 30)
|
||||
@ -703,7 +718,8 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse,
|
||||
company=company)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture'))
|
||||
self.assertRaises(frappe.ValidationError, stock_entry.save)
|
||||
|
||||
def test_wo_completion_with_pl_bom(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import (
|
||||
|
@ -8,6 +8,8 @@ from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
@ -74,7 +76,6 @@ class WorkOrder(Document):
|
||||
|
||||
self.set_required_items(reset_only_qty = len(self.get("required_items")))
|
||||
|
||||
|
||||
def validate_sales_order(self):
|
||||
if self.sales_order:
|
||||
self.check_sales_order_on_hold_or_close()
|
||||
@ -271,7 +272,7 @@ class WorkOrder(Document):
|
||||
|
||||
produced_qty = total_qty[0][0] if total_qty else 0
|
||||
|
||||
production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
|
||||
production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item)
|
||||
|
||||
def before_submit(self):
|
||||
self.create_serial_no_batch_no()
|
||||
@ -544,7 +545,7 @@ class WorkOrder(Document):
|
||||
if node.is_bom:
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty))
|
||||
|
||||
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
|
||||
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
||||
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
|
||||
|
||||
for correct_index, operation in enumerate(operations, start=1):
|
||||
@ -625,7 +626,7 @@ class WorkOrder(Document):
|
||||
frappe.delete_doc("Job Card", d.name)
|
||||
|
||||
def validate_production_item(self):
|
||||
if frappe.db.get_value("Item", self.production_item, "has_variants"):
|
||||
if frappe.get_cached_value("Item", self.production_item, "has_variants"):
|
||||
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
|
||||
|
||||
if self.production_item:
|
||||
@ -1175,3 +1176,27 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
||||
doc.set_item_locations()
|
||||
|
||||
return doc
|
||||
|
||||
def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
|
||||
"""Get total reserved quantity for any item in specified warehouse"""
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
wo_item = frappe.qb.DocType("Work Order Item")
|
||||
|
||||
return (
|
||||
frappe.qb
|
||||
.from_(wo)
|
||||
.from_(wo_item)
|
||||
.select(Sum(Case()
|
||||
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
|
||||
.else_(wo_item.required_qty - wo_item.consumed_qty))
|
||||
)
|
||||
.where(
|
||||
(wo_item.item_code == item_code)
|
||||
& (wo_item.parent == wo.name)
|
||||
& (wo.docstatus == 1)
|
||||
& (wo_item.source_warehouse == warehouse)
|
||||
& (wo.status.notin(["Stopped", "Completed", "Closed"]))
|
||||
& ((wo_item.required_qty > wo_item.transferred_qty)
|
||||
| (wo_item.required_qty > wo_item.consumed_qty))
|
||||
)
|
||||
).run()[0][0] or 0.0
|
||||
|
@ -89,10 +89,10 @@ def get_bom_stock(filters):
|
||||
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
|
||||
|
||||
def get_manufacturer_records():
|
||||
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
|
||||
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"])
|
||||
manufacture_details = frappe._dict()
|
||||
for detail in details:
|
||||
dic = manufacture_details.setdefault(detail.get('parent'), {})
|
||||
dic = manufacture_details.setdefault(detail.get('item_code'), {})
|
||||
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
|
||||
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
|
||||
|
||||
|
@ -172,10 +172,15 @@ class ProductionPlanReport(object):
|
||||
|
||||
self.purchase_details = {}
|
||||
|
||||
for d in frappe.get_all("Purchase Order Item",
|
||||
purchased_items = frappe.get_all("Purchase Order Item",
|
||||
fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"],
|
||||
filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)},
|
||||
group_by = "item_code, warehouse"):
|
||||
filters={
|
||||
"item_code": ("in", self.item_codes),
|
||||
"warehouse": ("in", self.warehouses),
|
||||
"docstatus": 1,
|
||||
},
|
||||
group_by = "item_code, warehouse")
|
||||
for d in purchased_items:
|
||||
key = (d.item_code, d.warehouse)
|
||||
if key not in self.purchase_details:
|
||||
self.purchase_details.setdefault(key, d)
|
||||
|
@ -21,4 +21,5 @@ Communication
|
||||
Loan Management
|
||||
Payroll
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
|
@ -349,3 +349,6 @@ erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
||||
erpnext.patches.v14_0.migrate_cost_center_allocations
|
||||
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||
erpnext.patches.v13_0.shopping_cart_to_ecommerce
|
||||
erpnext.patches.v13_0.update_disbursement_account
|
||||
erpnext.patches.v13_0.update_reserved_qty_closed_wo
|
||||
erpnext.patches.v14_0.delete_amazon_mws_doctype
|
||||
|
@ -1,12 +0,0 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0]
|
||||
if count == 0:
|
||||
frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';")
|
||||
|
||||
frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings")
|
22
erpnext/patches/v13_0/update_disbursement_account.py
Normal file
22
erpnext/patches/v13_0/update_disbursement_account.py
Normal file
@ -0,0 +1,22 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
frappe.reload_doc("loan_management", "doctype", "loan_type")
|
||||
frappe.reload_doc("loan_management", "doctype", "loan")
|
||||
|
||||
loan_type = frappe.qb.DocType("Loan Type")
|
||||
loan = frappe.qb.DocType("Loan")
|
||||
|
||||
frappe.qb.update(
|
||||
loan_type
|
||||
).set(
|
||||
loan_type.disbursement_account, loan_type.payment_account
|
||||
).run()
|
||||
|
||||
frappe.qb.update(
|
||||
loan
|
||||
).set(
|
||||
loan.disbursement_account, loan.payment_account
|
||||
).run()
|
28
erpnext/patches/v13_0/update_reserved_qty_closed_wo.py
Normal file
28
erpnext/patches/v13_0/update_reserved_qty_closed_wo.py
Normal file
@ -0,0 +1,28 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.stock.utils import get_bin
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
wo_item = frappe.qb.DocType("Work Order Item")
|
||||
|
||||
incorrect_item_wh = (
|
||||
frappe.qb
|
||||
.from_(wo)
|
||||
.join(wo_item).on(wo.name == wo_item.parent)
|
||||
.select(wo_item.item_code, wo.source_warehouse).distinct()
|
||||
.where(
|
||||
(wo.status == "Closed")
|
||||
& (wo.docstatus == 1)
|
||||
& (wo.source_warehouse.notnull())
|
||||
)
|
||||
).run()
|
||||
|
||||
for item_code, warehouse in incorrect_item_wh:
|
||||
if not (item_code and warehouse):
|
||||
continue
|
||||
|
||||
bin = get_bin(item_code, warehouse)
|
||||
bin.update_reserved_qty_for_production()
|
5
erpnext/patches/v14_0/delete_amazon_mws_doctype.py
Normal file
5
erpnext/patches/v14_0/delete_amazon_mws_doctype.py
Normal file
@ -0,0 +1,5 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.delete_doc("DocType", "Amazon MWS Settings", ignore_missing=True)
|
@ -3,6 +3,14 @@
|
||||
|
||||
frappe.ui.form.on('Gratuity', {
|
||||
setup: function (frm) {
|
||||
frm.set_query("salary_component", function () {
|
||||
return {
|
||||
filters: {
|
||||
type: "Earning"
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("expense_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
@ -24,7 +32,7 @@ frappe.ui.form.on('Gratuity', {
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.status == "Unpaid") {
|
||||
if (frm.doc.docstatus == 1 && !frm.doc.pay_via_salary_slip && frm.doc.status == "Unpaid") {
|
||||
frm.add_custom_button(__("Create Payment Entry"), function () {
|
||||
return frappe.call({
|
||||
method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "HR-GRA-PAY-.#####",
|
||||
"creation": "2020-08-05 20:52:13.024683",
|
||||
"creation": "2022-01-27 16:24:28.200061",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
@ -16,6 +16,9 @@
|
||||
"company",
|
||||
"gratuity_rule",
|
||||
"section_break_5",
|
||||
"pay_via_salary_slip",
|
||||
"payroll_date",
|
||||
"salary_component",
|
||||
"payable_account",
|
||||
"expense_account",
|
||||
"mode_of_payment",
|
||||
@ -78,18 +81,20 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Expense Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
"fieldname": "mode_of_payment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
"options": "Mode of Payment"
|
||||
},
|
||||
{
|
||||
"fieldname": "gratuity_rule",
|
||||
@ -151,23 +156,45 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
"fieldname": "payable_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payable Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "pay_via_salary_slip",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pay via Salary Slip"
|
||||
},
|
||||
{
|
||||
"depends_on": "pay_via_salary_slip",
|
||||
"fieldname": "payroll_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Payroll Date",
|
||||
"mandatory_depends_on": "pay_via_salary_slip"
|
||||
},
|
||||
{
|
||||
"depends_on": "pay_via_salary_slip",
|
||||
"fieldname": "salary_component",
|
||||
"fieldtype": "Link",
|
||||
"label": "Salary Component",
|
||||
"mandatory_depends_on": "pay_via_salary_slip",
|
||||
"options": "Salary Component"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-19 12:54:37.306145",
|
||||
"modified": "2022-02-02 14:00:45.536152",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Gratuity",
|
||||
|
@ -21,7 +21,10 @@ class Gratuity(AccountsController):
|
||||
self.status = "Unpaid"
|
||||
|
||||
def on_submit(self):
|
||||
self.create_gl_entries()
|
||||
if self.pay_via_salary_slip:
|
||||
self.create_additional_salary()
|
||||
else:
|
||||
self.create_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ['GL Entry']
|
||||
@ -64,6 +67,19 @@ class Gratuity(AccountsController):
|
||||
|
||||
return gl_entry
|
||||
|
||||
def create_additional_salary(self):
|
||||
if self.pay_via_salary_slip:
|
||||
additional_salary = frappe.new_doc('Additional Salary')
|
||||
additional_salary.employee = self.employee
|
||||
additional_salary.salary_component = self.salary_component
|
||||
additional_salary.overwrite_salary_structure_amount = 0
|
||||
additional_salary.amount = self.amount
|
||||
additional_salary.payroll_date = self.payroll_date
|
||||
additional_salary.company = self.company
|
||||
additional_salary.ref_doctype = self.doctype
|
||||
additional_salary.ref_docname = self.name
|
||||
additional_salary.submit()
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
paid_amount = frappe.db.sql("""
|
||||
select ifnull(sum(debit_in_account_currency), 0) as paid_amount
|
||||
|
@ -10,7 +10,7 @@ def get_data():
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Payment'),
|
||||
'items': ['Payment Entry']
|
||||
'items': ['Payment Entry', 'Additional Salary']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -18,27 +18,25 @@ from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
|
||||
|
||||
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
|
||||
class TestGratuity(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Gratuity")
|
||||
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
|
||||
|
||||
make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
|
||||
make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.sql("DELETE FROM `tabGratuity`")
|
||||
|
||||
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
||||
new_employee = make_employee("new_employee@salary.com", company='_Test Company')
|
||||
salary_slip = get_last_salary_slip(new_employee)
|
||||
assert salary_slip is None
|
||||
|
||||
def test_check_gratuity_amount_based_on_current_slab(self):
|
||||
def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
|
||||
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
|
||||
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
|
||||
|
||||
gratuity = create_gratuity(employee=employee, rule=rule.name)
|
||||
|
||||
#work experience calculation
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
|
||||
|
||||
@ -64,6 +62,9 @@ class TestGratuity(unittest.TestCase):
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
# additional salary creation (Pay via salary slip)
|
||||
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
|
||||
|
||||
def test_check_gratuity_amount_based_on_all_previous_slabs(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
|
||||
@ -117,8 +118,8 @@ class TestGratuity(unittest.TestCase):
|
||||
self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.sql("DELETE FROM `tabGratuity`")
|
||||
frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def get_gratuity_rule(name):
|
||||
rule = frappe.db.exists("Gratuity Rule", name)
|
||||
@ -141,9 +142,14 @@ def create_gratuity(**args):
|
||||
gratuity.employee = args.employee
|
||||
gratuity.posting_date = getdate()
|
||||
gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)"
|
||||
gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
|
||||
gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
|
||||
gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
|
||||
gratuity.pay_via_salary_slip = args.pay_via_salary_slip or 0
|
||||
if gratuity.pay_via_salary_slip:
|
||||
gratuity.payroll_date = getdate()
|
||||
gratuity.salary_component = "Performance Bonus"
|
||||
else:
|
||||
gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
|
||||
gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
|
||||
gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
|
||||
|
||||
gratuity.save()
|
||||
gratuity.submit()
|
||||
|
@ -527,11 +527,12 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
|
||||
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
|
||||
|
||||
def remove_payrolled_employees(emp_list, start_date, end_date):
|
||||
new_emp_list = []
|
||||
for employee_details in emp_list:
|
||||
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}):
|
||||
emp_list.remove(employee_details)
|
||||
if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}):
|
||||
new_emp_list.append(employee_details)
|
||||
|
||||
return emp_list
|
||||
return new_emp_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_start_end_dates(payroll_frequency, start_date=None, company=None):
|
||||
|
@ -124,7 +124,7 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
|
||||
if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
|
||||
create_account(account_name="_Test Payroll Payable",
|
||||
company="_Test Company", parent_account="Current Liabilities - _TC")
|
||||
company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable")
|
||||
|
||||
if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \
|
||||
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
|
||||
@ -214,6 +214,7 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
create_loan_type("Car Loan", 500000, 8.4,
|
||||
is_term_loan=1,
|
||||
mode_of_payment='Cash',
|
||||
disbursement_account='Disbursement Account - _TC',
|
||||
payment_account='Payment Account - _TC',
|
||||
loan_account='Loan Account - _TC',
|
||||
interest_income_account='Interest Income Account - _TC',
|
||||
|
@ -6,6 +6,7 @@ import random
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@ -370,6 +371,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
create_loan_type("Car Loan", 500000, 8.4,
|
||||
is_term_loan=1,
|
||||
mode_of_payment='Cash',
|
||||
disbursement_account='Disbursement Account - _TC',
|
||||
payment_account='Payment Account - _TC',
|
||||
loan_account='Loan Account - _TC',
|
||||
interest_income_account='Interest Income Account - _TC',
|
||||
@ -686,20 +688,25 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
|
||||
def make_salary_component(salary_components, test_tax, company_list=None):
|
||||
for salary_component in salary_components:
|
||||
if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
|
||||
if test_tax:
|
||||
if salary_component["type"] == "Earning":
|
||||
salary_component["is_tax_applicable"] = 1
|
||||
elif salary_component["salary_component"] == "TDS":
|
||||
salary_component["variable_based_on_taxable_salary"] = 1
|
||||
salary_component["amount_based_on_formula"] = 0
|
||||
salary_component["amount"] = 0
|
||||
salary_component["formula"] = ""
|
||||
salary_component["condition"] = ""
|
||||
salary_component["doctype"] = "Salary Component"
|
||||
salary_component["salary_component_abbr"] = salary_component["abbr"]
|
||||
frappe.get_doc(salary_component).insert()
|
||||
get_salary_component_account(salary_component["salary_component"], company_list)
|
||||
if frappe.db.exists('Salary Component', salary_component["salary_component"]):
|
||||
continue
|
||||
|
||||
if test_tax:
|
||||
if salary_component["type"] == "Earning":
|
||||
salary_component["is_tax_applicable"] = 1
|
||||
elif salary_component["salary_component"] == "TDS":
|
||||
salary_component["variable_based_on_taxable_salary"] = 1
|
||||
salary_component["amount_based_on_formula"] = 0
|
||||
salary_component["amount"] = 0
|
||||
salary_component["formula"] = ""
|
||||
salary_component["condition"] = ""
|
||||
|
||||
salary_component["salary_component_abbr"] = salary_component["abbr"]
|
||||
doc = frappe.new_doc("Salary Component")
|
||||
doc.update(salary_component)
|
||||
doc.insert()
|
||||
|
||||
get_salary_component_account(doc, company_list)
|
||||
|
||||
def get_salary_component_account(sal_comp, company_list=None):
|
||||
company = erpnext.get_default_company()
|
||||
@ -707,7 +714,9 @@ def get_salary_component_account(sal_comp, company_list=None):
|
||||
if company_list and company not in company_list:
|
||||
company_list.append(company)
|
||||
|
||||
sal_comp = frappe.get_doc("Salary Component", sal_comp)
|
||||
if not isinstance(sal_comp, Document):
|
||||
sal_comp = frappe.get_doc("Salary Component", sal_comp)
|
||||
|
||||
if not sal_comp.get("accounts"):
|
||||
for d in company_list:
|
||||
company_abbr = frappe.get_cached_value('Company', d, 'abbr')
|
||||
@ -725,7 +734,7 @@ def get_salary_component_account(sal_comp, company_list=None):
|
||||
})
|
||||
sal_comp.save()
|
||||
|
||||
def create_account(account_name, company, parent_account):
|
||||
def create_account(account_name, company, parent_account, account_type=None):
|
||||
company_abbr = frappe.get_cached_value('Company', company, 'abbr')
|
||||
account = frappe.db.get_value("Account", account_name + " - " + company_abbr)
|
||||
if not account:
|
||||
|
@ -58,6 +58,7 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Yes",
|
||||
"fieldname": "is_active",
|
||||
"fieldtype": "Select",
|
||||
@ -232,10 +233,11 @@
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-31 15:41:12.342380",
|
||||
"modified": "2022-02-03 23:50:10.205676",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Salary Structure",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -271,5 +273,6 @@
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -39,7 +39,8 @@
|
||||
"public/js/utils/dimension_tree_filter.js",
|
||||
"public/js/telephony.js",
|
||||
"public/js/templates/call_link.html",
|
||||
"public/js/templates/node_card.html"
|
||||
"public/js/templates/node_card.html",
|
||||
"public/js/bulk_transaction_processing.js"
|
||||
],
|
||||
"js/item-dashboard.min.js": [
|
||||
"stock/dashboard/item_dashboard.html",
|
||||
|
30
erpnext/public/js/bulk_transaction_processing.js
Normal file
30
erpnext/public/js/bulk_transaction_processing.js
Normal file
@ -0,0 +1,30 @@
|
||||
frappe.provide("erpnext.bulk_transaction_processing");
|
||||
|
||||
$.extend(erpnext.bulk_transaction_processing, {
|
||||
create: function(listview, from_doctype, to_doctype) {
|
||||
let checked_items = listview.get_checked_items();
|
||||
const doc_name = [];
|
||||
checked_items.forEach((Item)=> {
|
||||
if (Item.docstatus == 0) {
|
||||
doc_name.push(Item.name);
|
||||
}
|
||||
});
|
||||
|
||||
let count_of_rows = checked_items.length;
|
||||
frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{
|
||||
if (doc_name.length == 0) {
|
||||
frappe.call({
|
||||
method: "erpnext.utilities.bulk_transaction.transaction_processing",
|
||||
args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype}
|
||||
}).then(()=> {
|
||||
|
||||
});
|
||||
if (count_of_rows > 10) {
|
||||
frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]);
|
||||
}
|
||||
} else {
|
||||
frappe.msgprint(__("Selected document must be in submitted state"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -1463,7 +1463,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"item_code": d.item_code,
|
||||
"pricing_rules": d.pricing_rules,
|
||||
"parenttype": d.parenttype,
|
||||
"parent": d.parent
|
||||
"parent": d.parent,
|
||||
"price_list_rate": d.price_list_rate
|
||||
})
|
||||
}
|
||||
});
|
||||
@ -2288,7 +2289,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
() => this.frm.doc.ignore_pricing_rule=1,
|
||||
() => me.ignore_pricing_rule(),
|
||||
() => this.frm.doc.ignore_pricing_rule=0,
|
||||
() => me.apply_pricing_rule()
|
||||
() => me.apply_pricing_rule(),
|
||||
() => this.frm.save()
|
||||
]);
|
||||
} else {
|
||||
frappe.run_serially([
|
||||
|
@ -22,5 +22,6 @@ import "./call_popup/call_popup";
|
||||
import "./utils/dimension_tree_filter";
|
||||
import "./telephony";
|
||||
import "./templates/call_link.html";
|
||||
import "./bulk_transaction_processing";
|
||||
|
||||
// import { sum } from 'frappe/public/utils/util.js'
|
||||
|
@ -295,6 +295,10 @@ class GSTR3BReport(Document):
|
||||
inter_state_supply_details = {}
|
||||
|
||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
|
||||
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
|
||||
export_type = self.invoice_detail_map.get(inv, {}).get('export_type')
|
||||
|
||||
for rate, items in items_based_on_rate.items():
|
||||
for item_code, taxable_value in self.invoice_items.get(inv).items():
|
||||
if item_code in items:
|
||||
@ -302,9 +306,8 @@ class GSTR3BReport(Document):
|
||||
self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
|
||||
elif item_code in self.is_non_gst:
|
||||
self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
|
||||
elif rate == 0:
|
||||
elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'):
|
||||
self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
|
||||
#self.report_dict['sup_details']['osup_zero'][key] += tax_amount
|
||||
else:
|
||||
if inv in self.cgst_sgst_invoices:
|
||||
tax_rate = rate/2
|
||||
@ -315,9 +318,6 @@ class GSTR3BReport(Document):
|
||||
self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
|
||||
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
|
||||
|
||||
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
|
||||
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
|
||||
|
||||
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
|
||||
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:
|
||||
inter_state_supply_details.setdefault((gst_category, place_of_supply), {
|
||||
|
@ -219,7 +219,6 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
|
||||
if not party_details.place_of_supply: return party_details
|
||||
if not party_details.company_gstin: return party_details
|
||||
if not party_details.supplier_gstin: return party_details
|
||||
|
||||
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
|
||||
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user